From 07ee667627bfbb6249dd0437239e5b0f2c80d6b8 Mon Sep 17 00:00:00 2001 From: xaviraol Date: Thu, 19 Dec 2019 15:12:28 +0100 Subject: [PATCH 01/13] gradle sync succesful --- android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + android/gradlew | 172 ++++++++++++++++++ android/gradlew.bat | 84 +++++++++ 4 files changed, 262 insertions(+) create mode 100644 android/gradle/wrapper/gradle-wrapper.jar create mode 100644 android/gradle/wrapper/gradle-wrapper.properties create mode 100644 android/gradlew create mode 100644 android/gradlew.bat diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..f6b961fd5a86aa5fbfe90f707c3138408be7c718 GIT binary patch literal 54329 zcmagFV|ZrKvM!pAZQHhO+qP}9lTNj?q^^Y^VFp)SH8qbSJ)2BQ2giqr}t zFG7D6)c?v~^Z#E_K}1nTQbJ9gQ9<%vVRAxVj)8FwL5_iTdUB>&m3fhE=kRWl;g`&m z!W5kh{WsV%fO*%je&j+Lv4xxK~zsEYQls$Q-p&dwID|A)!7uWtJF-=Tm1{V@#x*+kUI$=%KUuf2ka zjiZ{oiL1MXE2EjciJM!jrjFNwCh`~hL>iemrqwqnX?T*MX;U>>8yRcZb{Oy+VKZos zLiFKYPw=LcaaQt8tj=eoo3-@bG_342HQ%?jpgAE?KCLEHC+DmjxAfJ%Og^$dpC8Xw zAcp-)tfJm}BPNq_+6m4gBgBm3+CvmL>4|$2N$^Bz7W(}fz1?U-u;nE`+9`KCLuqg} zwNstNM!J4Uw|78&Y9~9>MLf56to!@qGkJw5Thx%zkzj%Ek9Nn1QA@8NBXbwyWC>9H z#EPwjMNYPigE>*Ofz)HfTF&%PFj$U6mCe-AFw$U%-L?~-+nSXHHKkdgC5KJRTF}`G zE_HNdrE}S0zf4j{r_f-V2imSqW?}3w-4=f@o@-q+cZgaAbZ((hn))@|eWWhcT2pLpTpL!;_5*vM=sRL8 zqU##{U#lJKuyqW^X$ETU5ETeEVzhU|1m1750#f}38_5N9)B_2|v@1hUu=Kt7-@dhA zq_`OMgW01n`%1dB*}C)qxC8q;?zPeF_r;>}%JYmlER_1CUbKa07+=TV45~symC*g8 zW-8(gag#cAOuM0B1xG8eTp5HGVLE}+gYTmK=`XVVV*U!>H`~j4+ROIQ+NkN$LY>h4 zqpwdeE_@AX@PL};e5vTn`Ro(EjHVf$;^oiA%@IBQq>R7_D>m2D4OwwEepkg}R_k*M zM-o;+P27087eb+%*+6vWFCo9UEGw>t&WI17Pe7QVuoAoGHdJ(TEQNlJOqnjZ8adCb zI`}op16D@v7UOEo%8E-~m?c8FL1utPYlg@m$q@q7%mQ4?OK1h%ODjTjFvqd!C z-PI?8qX8{a@6d&Lb_X+hKxCImb*3GFemm?W_du5_&EqRq!+H?5#xiX#w$eLti-?E$;Dhu`{R(o>LzM4CjO>ICf z&DMfES#FW7npnbcuqREgjPQM#gs6h>`av_oEWwOJZ2i2|D|0~pYd#WazE2Bbsa}X@ zu;(9fi~%!VcjK6)?_wMAW-YXJAR{QHxrD5g(ou9mR6LPSA4BRG1QSZT6A?kelP_g- zH(JQjLc!`H4N=oLw=f3{+WmPA*s8QEeEUf6Vg}@!xwnsnR0bl~^2GSa5vb!Yl&4!> zWb|KQUsC$lT=3A|7vM9+d;mq=@L%uWKwXiO9}a~gP4s_4Yohc!fKEgV7WbVo>2ITbE*i`a|V!^p@~^<={#?Gz57 zyPWeM2@p>D*FW#W5Q`1`#5NW62XduP1XNO(bhg&cX`-LYZa|m-**bu|>}S;3)eP8_ zpNTnTfm8 ze+7wDH3KJ95p)5tlwk`S7mbD`SqHnYD*6`;gpp8VdHDz%RR_~I_Ar>5)vE-Pgu7^Y z|9Px+>pi3!DV%E%4N;ii0U3VBd2ZJNUY1YC^-e+{DYq+l@cGtmu(H#Oh%ibUBOd?C z{y5jW3v=0eV0r@qMLgv1JjZC|cZ9l9Q)k1lLgm))UR@#FrJd>w^`+iy$c9F@ic-|q zVHe@S2UAnc5VY_U4253QJxm&Ip!XKP8WNcnx9^cQ;KH6PlW8%pSihSH2(@{2m_o+m zr((MvBja2ctg0d0&U5XTD;5?d?h%JcRJp{_1BQW1xu&BrA3(a4Fh9hon-ly$pyeHq zG&;6q?m%NJ36K1Sq_=fdP(4f{Hop;_G_(i?sPzvB zDM}>*(uOsY0I1j^{$yn3#U(;B*g4cy$-1DTOkh3P!LQ;lJlP%jY8}Nya=h8$XD~%Y zbV&HJ%eCD9nui-0cw!+n`V~p6VCRqh5fRX z8`GbdZ@73r7~myQLBW%db;+BI?c-a>Y)m-FW~M=1^|<21_Sh9RT3iGbO{o-hpN%d6 z7%++#WekoBOP^d0$$|5npPe>u3PLvX_gjH2x(?{&z{jJ2tAOWTznPxv-pAv<*V7r$ z6&glt>7CAClWz6FEi3bToz-soY^{ScrjwVPV51=>n->c(NJngMj6TyHty`bfkF1hc zkJS%A@cL~QV0-aK4>Id!9dh7>0IV;1J9(myDO+gv76L3NLMUm9XyPauvNu$S<)-|F zZS}(kK_WnB)Cl`U?jsdYfAV4nrgzIF@+%1U8$poW&h^c6>kCx3;||fS1_7JvQT~CV zQ8Js+!p)3oW>Df(-}uqC`Tcd%E7GdJ0p}kYj5j8NKMp(KUs9u7?jQ94C)}0rba($~ zqyBx$(1ae^HEDG`Zc@-rXk1cqc7v0wibOR4qpgRDt#>-*8N3P;uKV0CgJE2SP>#8h z=+;i_CGlv+B^+$5a}SicVaSeaNn29K`C&=}`=#Nj&WJP9Xhz4mVa<+yP6hkrq1vo= z1rX4qg8dc4pmEvq%NAkpMK>mf2g?tg_1k2%v}<3`$6~Wlq@ItJ*PhHPoEh1Yi>v57 z4k0JMO)*=S`tKvR5gb-(VTEo>5Y>DZJZzgR+j6{Y`kd|jCVrg!>2hVjz({kZR z`dLlKhoqT!aI8=S+fVp(5*Dn6RrbpyO~0+?fy;bm$0jmTN|t5i6rxqr4=O}dY+ROd zo9Et|x}!u*xi~>-y>!M^+f&jc;IAsGiM_^}+4|pHRn{LThFFpD{bZ|TA*wcGm}XV^ zr*C6~@^5X-*R%FrHIgo-hJTBcyQ|3QEj+cSqp#>&t`ZzB?cXM6S(lRQw$I2?m5=wd z78ki`R?%;o%VUhXH?Z#(uwAn9$m`npJ=cA+lHGk@T7qq_M6Zoy1Lm9E0UUysN)I_x zW__OAqvku^>`J&CB=ie@yNWsaFmem}#L3T(x?a`oZ+$;3O-icj2(5z72Hnj=9Z0w% z<2#q-R=>hig*(t0^v)eGq2DHC%GymE-_j1WwBVGoU=GORGjtaqr0BNigOCqyt;O(S zKG+DoBsZU~okF<7ahjS}bzwXxbAxFfQAk&O@>LsZMsZ`?N?|CDWM(vOm%B3CBPC3o z%2t@%H$fwur}SSnckUm0-k)mOtht`?nwsDz=2#v=RBPGg39i#%odKq{K^;bTD!6A9 zskz$}t)sU^=a#jLZP@I=bPo?f-L}wpMs{Tc!m7-bi!Ldqj3EA~V;4(dltJmTXqH0r z%HAWKGutEc9vOo3P6Q;JdC^YTnby->VZ6&X8f{obffZ??1(cm&L2h7q)*w**+sE6dG*;(H|_Q!WxU{g)CeoT z(KY&bv!Usc|m+Fqfmk;h&RNF|LWuNZ!+DdX*L=s-=_iH=@i` z?Z+Okq^cFO4}_n|G*!)Wl_i%qiMBaH8(WuXtgI7EO=M>=i_+;MDjf3aY~6S9w0K zUuDO7O5Ta6+k40~xh~)D{=L&?Y0?c$s9cw*Ufe18)zzk%#ZY>Tr^|e%8KPb0ht`b( zuP@8#Ox@nQIqz9}AbW0RzE`Cf>39bOWz5N3qzS}ocxI=o$W|(nD~@EhW13Rj5nAp; zu2obEJa=kGC*#3=MkdkWy_%RKcN=?g$7!AZ8vBYKr$ePY(8aIQ&yRPlQ=mudv#q$q z4%WzAx=B{i)UdLFx4os?rZp6poShD7Vc&mSD@RdBJ=_m^&OlkEE1DFU@csgKcBifJ zz4N7+XEJhYzzO=86 z#%eBQZ$Nsf2+X0XPHUNmg#(sNt^NW1Y0|M(${e<0kW6f2q5M!2YE|hSEQ*X-%qo(V zHaFwyGZ0on=I{=fhe<=zo{=Og-_(to3?cvL4m6PymtNsdDINsBh8m>a%!5o3s(en) z=1I z6O+YNertC|OFNqd6P=$gMyvmfa`w~p9*gKDESFqNBy(~Zw3TFDYh}$iudn)9HxPBi zdokK@o~nu?%imcURr5Y~?6oo_JBe}t|pU5qjai|#JDyG=i^V~7+a{dEnO<(y>ahND#_X_fcEBNiZ)uc&%1HVtx8Ts z*H_Btvx^IhkfOB#{szN*n6;y05A>3eARDXslaE>tnLa>+`V&cgho?ED+&vv5KJszf zG4@G;7i;4_bVvZ>!mli3j7~tPgybF5|J6=Lt`u$D%X0l}#iY9nOXH@(%FFJLtzb%p zzHfABnSs;v-9(&nzbZytLiqqDIWzn>JQDk#JULcE5CyPq_m#4QV!}3421haQ+LcfO*>r;rg6K|r#5Sh|y@h1ao%Cl)t*u`4 zMTP!deC?aL7uTxm5^nUv#q2vS-5QbBKP|drbDXS%erB>fYM84Kpk^au99-BQBZR z7CDynflrIAi&ahza+kUryju5LR_}-Z27g)jqOc(!Lx9y)e z{cYc&_r947s9pteaa4}dc|!$$N9+M38sUr7h(%@Ehq`4HJtTpA>B8CLNO__@%(F5d z`SmX5jbux6i#qc}xOhumzbAELh*Mfr2SW99=WNOZRZgoCU4A2|4i|ZVFQt6qEhH#B zK_9G;&h*LO6tB`5dXRSBF0hq0tk{2q__aCKXYkP#9n^)@cq}`&Lo)1KM{W+>5mSed zKp~=}$p7>~nK@va`vN{mYzWN1(tE=u2BZhga5(VtPKk(*TvE&zmn5vSbjo zZLVobTl%;t@6;4SsZ>5+U-XEGUZGG;+~|V(pE&qqrp_f~{_1h@5ZrNETqe{bt9ioZ z#Qn~gWCH!t#Ha^n&fT2?{`}D@s4?9kXj;E;lWV9Zw8_4yM0Qg-6YSsKgvQ*fF{#Pq z{=(nyV>#*`RloBVCs;Lp*R1PBIQOY=EK4CQa*BD0MsYcg=opP?8;xYQDSAJBeJpw5 zPBc_Ft9?;<0?pBhCmOtWU*pN*;CkjJ_}qVic`}V@$TwFi15!mF1*m2wVX+>5p%(+R zQ~JUW*zWkalde{90@2v+oVlkxOZFihE&ZJ){c?hX3L2@R7jk*xjYtHi=}qb+4B(XJ z$gYcNudR~4Kz_WRq8eS((>ALWCO)&R-MXE+YxDn9V#X{_H@j616<|P(8h(7z?q*r+ zmpqR#7+g$cT@e&(%_|ipI&A%9+47%30TLY(yuf&*knx1wNx|%*H^;YB%ftt%5>QM= z^i;*6_KTSRzQm%qz*>cK&EISvF^ovbS4|R%)zKhTH_2K>jP3mBGn5{95&G9^a#4|K zv+!>fIsR8z{^x4)FIr*cYT@Q4Z{y}};rLHL+atCgHbfX*;+k&37DIgENn&=k(*lKD zG;uL-KAdLn*JQ?@r6Q!0V$xXP=J2i~;_+i3|F;_En;oAMG|I-RX#FwnmU&G}w`7R{ z788CrR-g1DW4h_`&$Z`ctN~{A)Hv_-Bl!%+pfif8wN32rMD zJDs$eVWBYQx1&2sCdB0!vU5~uf)=vy*{}t{2VBpcz<+~h0wb7F3?V^44*&83Z2#F` z32!rd4>uc63rQP$3lTH3zb-47IGR}f)8kZ4JvX#toIpXH`L%NnPDE~$QI1)0)|HS4 zVcITo$$oWWwCN@E-5h>N?Hua!N9CYb6f8vTFd>h3q5Jg-lCI6y%vu{Z_Uf z$MU{{^o~;nD_@m2|E{J)q;|BK7rx%`m``+OqZAqAVj-Dy+pD4-S3xK?($>wn5bi90CFAQ+ACd;&m6DQB8_o zjAq^=eUYc1o{#+p+ zn;K<)Pn*4u742P!;H^E3^Qu%2dM{2slouc$AN_3V^M7H_KY3H)#n7qd5_p~Za7zAj|s9{l)RdbV9e||_67`#Tu*c<8!I=zb@ z(MSvQ9;Wrkq6d)!9afh+G`!f$Ip!F<4ADdc*OY-y7BZMsau%y?EN6*hW4mOF%Q~bw z2==Z3^~?q<1GTeS>xGN-?CHZ7a#M4kDL zQxQr~1ZMzCSKFK5+32C%+C1kE#(2L=15AR!er7GKbp?Xd1qkkGipx5Q~FI-6zt< z*PTpeVI)Ngnnyaz5noIIgNZtb4bQdKG{Bs~&tf)?nM$a;7>r36djllw%hQxeCXeW^ z(i6@TEIuxD<2ulwLTt|&gZP%Ei+l!(%p5Yij6U(H#HMkqM8U$@OKB|5@vUiuY^d6X zW}fP3;Kps6051OEO(|JzmVU6SX(8q>*yf*x5QoxDK={PH^F?!VCzES_Qs>()_y|jg6LJlJWp;L zKM*g5DK7>W_*uv}{0WUB0>MHZ#oJZmO!b3MjEc}VhsLD~;E-qNNd?x7Q6~v zR=0$u>Zc2Xr}>x_5$-s#l!oz6I>W?lw;m9Ae{Tf9eMX;TI-Wf_mZ6sVrMnY#F}cDd z%CV*}fDsXUF7Vbw>PuDaGhu631+3|{xp<@Kl|%WxU+vuLlcrklMC!Aq+7n~I3cmQ! z`e3cA!XUEGdEPSu``&lZEKD1IKO(-VGvcnSc153m(i!8ohi`)N2n>U_BemYJ`uY>8B*Epj!oXRLV}XK}>D*^DHQ7?NY*&LJ9VSo`Ogi9J zGa;clWI8vIQqkngv2>xKd91K>?0`Sw;E&TMg&6dcd20|FcTsnUT7Yn{oI5V4@Ow~m zz#k~8TM!A9L7T!|colrC0P2WKZW7PNj_X4MfESbt<-soq*0LzShZ}fyUx!(xIIDwx zRHt^_GAWe0-Vm~bDZ(}XG%E+`XhKpPlMBo*5q_z$BGxYef8O!ToS8aT8pmjbPq)nV z%x*PF5ZuSHRJqJ!`5<4xC*xb2vC?7u1iljB_*iUGl6+yPyjn?F?GOF2_KW&gOkJ?w z3e^qc-te;zez`H$rsUCE0<@7PKGW?7sT1SPYWId|FJ8H`uEdNu4YJjre`8F*D}6Wh z|FQ`xf7yiphHIAkU&OYCn}w^ilY@o4larl?^M7&8YI;hzBIsX|i3UrLsx{QDKwCX< zy;a>yjfJ6!sz`NcVi+a!Fqk^VE^{6G53L?@Tif|j!3QZ0fk9QeUq8CWI;OmO-Hs+F zuZ4sHLA3{}LR2Qlyo+{d@?;`tpp6YB^BMoJt?&MHFY!JQwoa0nTSD+#Ku^4b{5SZVFwU9<~APYbaLO zu~Z)nS#dxI-5lmS-Bnw!(u15by(80LlC@|ynj{TzW)XcspC*}z0~8VRZq>#Z49G`I zgl|C#H&=}n-ajxfo{=pxPV(L*7g}gHET9b*s=cGV7VFa<;Htgjk>KyW@S!|z`lR1( zGSYkEl&@-bZ*d2WQ~hw3NpP=YNHF^XC{TMG$Gn+{b6pZn+5=<()>C!N^jncl0w6BJ zdHdnmSEGK5BlMeZD!v4t5m7ct7{k~$1Ie3GLFoHjAH*b?++s<|=yTF+^I&jT#zuMx z)MLhU+;LFk8bse|_{j+d*a=&cm2}M?*arjBPnfPgLwv)86D$6L zLJ0wPul7IenMvVAK$z^q5<^!)7aI|<&GGEbOr=E;UmGOIa}yO~EIr5xWU_(ol$&fa zR5E(2vB?S3EvJglTXdU#@qfDbCYs#82Yo^aZN6`{Ex#M)easBTe_J8utXu(fY1j|R z9o(sQbj$bKU{IjyhosYahY{63>}$9_+hWxB3j}VQkJ@2$D@vpeRSldU?&7I;qd2MF zSYmJ>zA(@N_iK}m*AMPIJG#Y&1KR)6`LJ83qg~`Do3v^B0>fU&wUx(qefuTgzFED{sJ65!iw{F2}1fQ3= ziFIP{kezQxmlx-!yo+sC4PEtG#K=5VM9YIN0z9~c4XTX?*4e@m;hFM!zVo>A`#566 z>f&3g94lJ{r)QJ5m7Xe3SLau_lOpL;A($wsjHR`;xTXgIiZ#o&vt~ zGR6KdU$FFbLfZCC3AEu$b`tj!9XgOGLSV=QPIYW zjI!hSP#?8pn0@ezuenOzoka8!8~jXTbiJ6+ZuItsWW03uzASFyn*zV2kIgPFR$Yzm zE<$cZlF>R8?Nr2_i?KiripBc+TGgJvG@vRTY2o?(_Di}D30!k&CT`>+7ry2!!iC*X z<@=U0_C#16=PN7bB39w+zPwDOHX}h20Ap);dx}kjXX0-QkRk=cr};GYsjSvyLZa-t zzHONWddi*)RDUH@RTAsGB_#&O+QJaaL+H<<9LLSE+nB@eGF1fALwjVOl8X_sdOYme z0lk!X=S(@25=TZHR7LlPp}fY~yNeThMIjD}pd9+q=j<_inh0$>mIzWVY+Z9p<{D^#0Xk+b_@eNSiR8;KzSZ#7lUsk~NGMcB8C2c=m2l5paHPq`q{S(kdA7Z1a zyfk2Y;w?^t`?@yC5Pz9&pzo}Hc#}mLgDmhKV|PJ3lKOY(Km@Fi2AV~CuET*YfUi}u zfInZnqDX(<#vaS<^fszuR=l)AbqG{}9{rnyx?PbZz3Pyu!eSJK`uwkJU!ORQXy4x83r!PNgOyD33}}L=>xX_93l6njNTuqL8J{l%*3FVn3MG4&Fv*`lBXZ z?=;kn6HTT^#SrPX-N)4EZiIZI!0ByXTWy;;J-Tht{jq1mjh`DSy7yGjHxIaY%*sTx zuy9#9CqE#qi>1misx=KRWm=qx4rk|}vd+LMY3M`ow8)}m$3Ggv&)Ri*ON+}<^P%T5 z_7JPVPfdM=Pv-oH<tecoE}(0O7|YZc*d8`Uv_M*3Rzv7$yZnJE6N_W=AQ3_BgU_TjA_T?a)U1csCmJ&YqMp-lJe`y6>N zt++Bi;ZMOD%%1c&-Q;bKsYg!SmS^#J@8UFY|G3!rtyaTFb!5@e(@l?1t(87ln8rG? z--$1)YC~vWnXiW3GXm`FNSyzu!m$qT=Eldf$sMl#PEfGmzQs^oUd=GIQfj(X=}dw+ zT*oa0*oS%@cLgvB&PKIQ=Ok?>x#c#dC#sQifgMwtAG^l3D9nIg(Zqi;D%807TtUUCL3_;kjyte#cAg?S%e4S2W>9^A(uy8Ss0Tc++ZTjJw1 z&Em2g!3lo@LlDyri(P^I8BPpn$RE7n*q9Q-c^>rfOMM6Pd5671I=ZBjAvpj8oIi$! zl0exNl(>NIiQpX~FRS9UgK|0l#s@#)p4?^?XAz}Gjb1?4Qe4?j&cL$C8u}n)?A@YC zfmbSM`Hl5pQFwv$CQBF=_$Sq zxsV?BHI5bGZTk?B6B&KLdIN-40S426X3j_|ceLla*M3}3gx3(_7MVY1++4mzhH#7# zD>2gTHy*%i$~}mqc#gK83288SKp@y3wz1L_e8fF$Rb}ex+`(h)j}%~Ld^3DUZkgez zOUNy^%>>HHE|-y$V@B}-M|_{h!vXpk01xaD%{l{oQ|~+^>rR*rv9iQen5t?{BHg|% zR`;S|KtUb!X<22RTBA4AAUM6#M?=w5VY-hEV)b`!y1^mPNEoy2K)a>OyA?Q~Q*&(O zRzQI~y_W=IPi?-OJX*&&8dvY0zWM2%yXdFI!D-n@6FsG)pEYdJbuA`g4yy;qrgR?G z8Mj7gv1oiWq)+_$GqqQ$(ZM@#|0j7})=#$S&hZwdoijFI4aCFLVI3tMH5fLreZ;KD zqA`)0l~D2tuIBYOy+LGw&hJ5OyE+@cnZ0L5+;yo2pIMdt@4$r^5Y!x7nHs{@>|W(MzJjATyWGNwZ^4j+EPU0RpAl-oTM@u{lx*i0^yyWPfHt6QwPvYpk9xFMWfBFt!+Gu6TlAmr zeQ#PX71vzN*_-xh&__N`IXv6`>CgV#eA_%e@7wjgkj8jlKzO~Ic6g$cT`^W{R{606 zCDP~+NVZ6DMO$jhL~#+!g*$T!XW63#(ngDn#Qwy71yj^gazS{e;3jGRM0HedGD@pt z?(ln3pCUA(ekqAvvnKy0G@?-|-dh=eS%4Civ&c}s%wF@0K5Bltaq^2Os1n6Z3%?-Q zAlC4goQ&vK6TpgtzkHVt*1!tBYt-`|5HLV1V7*#45Vb+GACuU+QB&hZ=N_flPy0TY zR^HIrdskB#<$aU;HY(K{a3(OQa$0<9qH(oa)lg@Uf>M5g2W0U5 zk!JSlhrw8quBx9A>RJ6}=;W&wt@2E$7J=9SVHsdC?K(L(KACb#z)@C$xXD8^!7|uv zZh$6fkq)aoD}^79VqdJ!Nz-8$IrU(_-&^cHBI;4 z^$B+1aPe|LG)C55LjP;jab{dTf$0~xbXS9!!QdcmDYLbL^jvxu2y*qnx2%jbL%rB z{aP85qBJe#(&O~Prk%IJARcdEypZ)vah%ZZ%;Zk{eW(U)Bx7VlzgOi8)x z`rh4l`@l_Ada7z&yUK>ZF;i6YLGwI*Sg#Fk#Qr0Jg&VLax(nNN$u-XJ5=MsP3|(lEdIOJ7|(x3iY;ea)5#BW*mDV%^=8qOeYO&gIdJVuLLN3cFaN=xZtFB=b zH{l)PZl_j^u+qx@89}gAQW7ofb+k)QwX=aegihossZq*+@PlCpb$rpp>Cbk9UJO<~ zDjlXQ_Ig#W0zdD3&*ei(FwlN#3b%FSR%&M^ywF@Fr>d~do@-kIS$e%wkIVfJ|Ohh=zc zF&Rnic^|>@R%v?@jO}a9;nY3Qrg_!xC=ZWUcYiA5R+|2nsM*$+c$TOs6pm!}Z}dfM zGeBhMGWw3$6KZXav^>YNA=r6Es>p<6HRYcZY)z{>yasbC81A*G-le8~QoV;rtKnkx z;+os8BvEe?0A6W*a#dOudsv3aWs?d% z0oNngyVMjavLjtjiG`!007#?62ClTqqU$@kIY`=x^$2e>iqIy1>o|@Tw@)P)B8_1$r#6>DB_5 zmaOaoE~^9TolgDgooKFuEFB#klSF%9-~d2~_|kQ0Y{Ek=HH5yq9s zDq#1S551c`kSiWPZbweN^A4kWiP#Qg6er1}HcKv{fxb1*BULboD0fwfaNM_<55>qM zETZ8TJDO4V)=aPp_eQjX%||Ud<>wkIzvDlpNjqW>I}W!-j7M^TNe5JIFh#-}zAV!$ICOju8Kx)N z0vLtzDdy*rQN!7r>Xz7rLw8J-(GzQlYYVH$WK#F`i_i^qVlzTNAh>gBWKV@XC$T-` z3|kj#iCquDhiO7NKum07i|<-NuVsX}Q}mIP$jBJDMfUiaWR3c|F_kWBMw0_Sr|6h4 zk`_r5=0&rCR^*tOy$A8K;@|NqwncjZ>Y-75vlpxq%Cl3EgH`}^^~=u zoll6xxY@a>0f%Ddpi;=cY}fyG!K2N-dEyXXmUP5u){4VnyS^T4?pjN@Ot4zjL(Puw z_U#wMH2Z#8Pts{olG5Dy0tZj;N@;fHheu>YKYQU=4Bk|wcD9MbA`3O4bj$hNRHwzb zSLcG0SLV%zywdbuwl(^E_!@&)TdXge4O{MRWk2RKOt@!8E{$BU-AH(@4{gxs=YAz9LIob|Hzto0}9cWoz6Tp2x0&xi#$ zHh$dwO&UCR1Ob2w00-2eG7d4=cN(Y>0R#$q8?||q@iTi+7-w-xR%uMr&StFIthC<# zvK(aPduwuNB}oJUV8+Zl)%cnfsHI%4`;x6XW^UF^e4s3Z@S<&EV8?56Wya;HNs0E> z`$0dgRdiUz9RO9Au3RmYq>K#G=X%*_dUbSJHP`lSfBaN8t-~@F>)BL1RT*9I851A3 z<-+Gb#_QRX>~av#Ni<#zLswtu-c6{jGHR>wflhKLzC4P@b%8&~u)fosoNjk4r#GvC zlU#UU9&0Hv;d%g72Wq?Ym<&&vtA3AB##L}=ZjiTR4hh7J)e>ei} zt*u+>h%MwN`%3}b4wYpV=QwbY!jwfIj#{me)TDOG`?tI!%l=AwL2G@9I~}?_dA5g6 zCKgK(;6Q0&P&K21Tx~k=o6jwV{dI_G+Ba*Zts|Tl6q1zeC?iYJTb{hel*x>^wb|2RkHkU$!+S4OU4ZOKPZjV>9OVsqNnv5jK8TRAE$A&^yRwK zj-MJ3Pl?)KA~fq#*K~W0l4$0=8GRx^9+?w z!QT8*-)w|S^B0)ZeY5gZPI2G(QtQf?DjuK(s^$rMA!C%P22vynZY4SuOE=wX2f8$R z)A}mzJi4WJnZ`!bHG1=$lwaxm!GOnRbR15F$nRC-M*H<*VfF|pQw(;tbSfp({>9^5 zw_M1-SJ9eGF~m(0dvp*P8uaA0Yw+EkP-SWqu zqal$hK8SmM7#Mrs0@OD+%_J%H*bMyZiWAZdsIBj#lkZ!l2c&IpLu(5^T0Ge5PHzR} zn;TXs$+IQ_&;O~u=Jz+XE0wbOy`=6>m9JVG} zJ~Kp1e5m?K3x@@>!D)piw^eMIHjD4RebtR`|IlckplP1;r21wTi8v((KqNqn%2CB< zifaQc&T}*M&0i|LW^LgdjIaX|o~I$`owHolRqeH_CFrqCUCleN130&vH}dK|^kC>) z-r2P~mApHotL4dRX$25lIcRh_*kJaxi^%ZN5-GAAMOxfB!6flLPY-p&QzL9TE%ho( zRwftE3sy5<*^)qYzKkL|rE>n@hyr;xPqncY6QJ8125!MWr`UCWuC~A#G1AqF1@V$kv>@NBvN&2ygy*{QvxolkRRb%Ui zsmKROR%{*g*WjUUod@@cS^4eF^}yQ1>;WlGwOli z+Y$(8I`0(^d|w>{eaf!_BBM;NpCoeem2>J}82*!em=}}ymoXk>QEfJ>G(3LNA2-46 z5PGvjr)Xh9>aSe>vEzM*>xp{tJyZox1ZRl}QjcvX2TEgNc^(_-hir@Es>NySoa1g^ zFow_twnHdx(j?Q_3q51t3XI7YlJ4_q&(0#)&a+RUy{IcBq?)eaWo*=H2UUVIqtp&lW9JTJiP&u zw8+4vo~_IJXZIJb_U^&=GI1nSD%e;P!c{kZALNCm5c%%oF+I3DrA63_@4)(v4(t~JiddILp7jmoy+>cD~ivwoctFfEL zP*#2Rx?_&bCpX26MBgp^4G>@h`Hxc(lnqyj!*t>9sOBcXN(hTwEDpn^X{x!!gPX?1 z*uM$}cYRwHXuf+gYTB}gDTcw{TXSOUU$S?8BeP&sc!Lc{{pEv}x#ELX>6*ipI1#>8 zKes$bHjiJ1OygZge_ak^Hz#k;=od1wZ=o71ba7oClBMq>Uk6hVq|ePPt)@FM5bW$I z;d2Or@wBjbTyZj|;+iHp%Bo!Vy(X3YM-}lasMItEV_QrP-Kk_J4C>)L&I3Xxj=E?| zsAF(IfVQ4w+dRRnJ>)}o^3_012YYgFWE)5TT=l2657*L8_u1KC>Y-R{7w^S&A^X^U}h20jpS zQsdeaA#WIE*<8KG*oXc~$izYilTc#z{5xhpXmdT-YUnGh9v4c#lrHG6X82F2-t35} zB`jo$HjKe~E*W$=g|j&P>70_cI`GnOQ;Jp*JK#CT zuEGCn{8A@bC)~0%wsEv?O^hSZF*iqjO~_h|>xv>PO+?525Nw2472(yqS>(#R)D7O( zg)Zrj9n9$}=~b00=Wjf?E418qP-@8%MQ%PBiCTX=$B)e5cHFDu$LnOeJ~NC;xmOk# z>z&TbsK>Qzk)!88lNI8fOE2$Uxso^j*1fz>6Ot49y@=po)j4hbTIcVR`ePHpuJSfp zxaD^Dn3X}Na3@<_Pc>a;-|^Pon(>|ytG_+U^8j_JxP=_d>L$Hj?|0lz>_qQ#a|$+( z(x=Lipuc8p4^}1EQhI|TubffZvB~lu$zz9ao%T?%ZLyV5S9}cLeT?c} z>yCN9<04NRi~1oR)CiBakoNhY9BPnv)kw%*iv8vdr&&VgLGIs(-FbJ?d_gfbL2={- zBk4lkdPk~7+jIxd4{M(-W1AC_WcN&Oza@jZoj zaE*9Y;g83#m(OhA!w~LNfUJNUuRz*H-=$s*z+q+;snKPRm9EptejugC-@7-a-}Tz0 z@KHra#Y@OXK+KsaSN9WiGf?&jlZ!V7L||%KHP;SLksMFfjkeIMf<1e~t?!G3{n)H8 zQAlFY#QwfKuj;l@<$YDATAk;%PtD%B(0<|8>rXU< zJ66rkAVW_~Dj!7JGdGGi4NFuE?7ZafdMxIh65Sz7yQoA7fBZCE@WwysB=+`kT^LFX zz8#FlSA5)6FG9(qL3~A24mpzL@@2D#>0J7mMS1T*9UJ zvOq!!a(%IYY69+h45CE?(&v9H4FCr>gK0>mK~F}5RdOuH2{4|}k@5XpsX7+LZo^Qa4sH5`eUj>iffoBVm+ zz4Mtf`h?NW$*q1yr|}E&eNl)J``SZvTf6Qr*&S%tVv_OBpbjnA0&Vz#(;QmGiq-k! zgS0br4I&+^2mgA15*~Cd00cXLYOLA#Ep}_)eED>m+K@JTPr_|lSN}(OzFXQSBc6fM z@f-%2;1@BzhZa*LFV z-LrLmkmB%<<&jEURBEW>soaZ*rSIJNwaV%-RSaCZi4X)qYy^PxZ=oL?6N-5OGOMD2 z;q_JK?zkwQ@b3~ln&sDtT5SpW9a0q+5Gm|fpVY2|zqlNYBR}E5+ahgdj!CvK$Tlk0 z9g$5N;aar=CqMsudQV>yb4l@hN(9Jcc=1(|OHsqH6|g=K-WBd8GxZ`AkT?OO z-z_Ued-??Z*R4~L7jwJ%-`s~FK|qNAJ;EmIVDVpk{Lr7T4l{}vL)|GuUuswe9c5F| zv*5%u01hlv08?00Vpwyk*Q&&fY8k6MjOfpZfKa@F-^6d=Zv|0@&4_544RP5(s|4VPVP-f>%u(J@23BHqo2=zJ#v9g=F!cP((h zpt0|(s++ej?|$;2PE%+kc6JMmJjDW)3BXvBK!h!E`8Y&*7hS{c_Z?4SFP&Y<3evqf z9-ke+bSj$%Pk{CJlJbWwlBg^mEC^@%Ou?o>*|O)rl&`KIbHrjcpqsc$Zqt0^^F-gU2O=BusO+(Op}!jNzLMc zT;0YT%$@ClS%V+6lMTfhuzzxomoat=1H?1$5Ei7&M|gxo`~{UiV5w64Np6xV zVK^nL$)#^tjhCpTQMspXI({TW^U5h&Wi1Jl8g?P1YCV4=%ZYyjSo#5$SX&`r&1PyC zzc;uzCd)VTIih|8eNqFNeBMe#j_FS6rq81b>5?aXg+E#&$m++Gz9<+2)h=K(xtn}F ziV{rmu+Y>A)qvF}ms}4X^Isy!M&1%$E!rTO~5(p+8{U6#hWu>(Ll1}eD64Xa>~73A*538wry?v$vW z>^O#FRdbj(k0Nr&)U`Tl(4PI*%IV~;ZcI2z&rmq=(k^}zGOYZF3b2~Klpzd2eZJl> zB=MOLwI1{$RxQ7Y4e30&yOx?BvAvDkTBvWPpl4V8B7o>4SJn*+h1Ms&fHso%XLN5j z-zEwT%dTefp~)J_C8;Q6i$t!dnlh-!%haR1X_NuYUuP-)`IGWjwzAvp!9@h`kPZhf zwLwFk{m3arCdx8rD~K2`42mIN4}m%OQ|f)4kf%pL?Af5Ul<3M2fv>;nlhEPR8b)u} zIV*2-wyyD%%) zl$G@KrC#cUwoL?YdQyf9WH)@gWB{jd5w4evI& zOFF)p_D8>;3-N1z6mES!OPe>B^<;9xsh)){Cw$Vs-ez5nXS95NOr3s$IU;>VZSzKn zBvub8_J~I%(DozZW@{)Vp37-zevxMRZ8$8iRfwHmYvyjOxIOAF2FUngKj289!(uxY zaClWm!%x&teKmr^ABrvZ(ikx{{I-lEzw5&4t3P0eX%M~>$wG0ZjA4Mb&op+0$#SO_ z--R`>X!aqFu^F|a!{Up-iF(K+alKB{MNMs>e(i@Tpy+7Z-dK%IEjQFO(G+2mOb@BO zP>WHlS#fSQm0et)bG8^ZDScGnh-qRKIFz zfUdnk=m){ej0i(VBd@RLtRq3Ep=>&2zZ2%&vvf?Iex01hx1X!8U+?>ER;yJlR-2q4 z;Y@hzhEC=d+Le%=esE>OQ!Q|E%6yG3V_2*uh&_nguPcZ{q?DNq8h_2ahaP6=pP-+x zK!(ve(yfoYC+n(_+chiJ6N(ZaN+XSZ{|H{TR1J_s8x4jpis-Z-rlRvRK#U%SMJ(`C z?T2 zF(NNfO_&W%2roEC2j#v*(nRgl1X)V-USp-H|CwFNs?n@&vpRcj@W@xCJwR6@T!jt377?XjZ06=`d*MFyTdyvW!`mQm~t3luzYzvh^F zM|V}rO>IlBjZc}9Z zd$&!tthvr>5)m;5;96LWiAV0?t)7suqdh0cZis`^Pyg@?t>Ms~7{nCU;z`Xl+raSr zXpp=W1oHB*98s!Tpw=R5C)O{{Inl>9l7M*kq%#w9a$6N~v?BY2GKOVRkXYCgg*d

<5G2M1WZP5 zzqSuO91lJod(SBDDw<*sX(+F6Uq~YAeYV#2A;XQu_p=N5X+#cmu19Qk>QAnV=k!?wbk5I;tDWgFc}0NkvC*G=V+Yh1cyeJVq~9czZiDXe+S=VfL2g`LWo8om z$Y~FQc6MFjV-t1Y`^D9XMwY*U_re2R?&(O~68T&D4S{X`6JYU-pz=}ew-)V0AOUT1 zVOkHAB-8uBcRjLvz<9HS#a@X*Kc@|W)nyiSgi|u5$Md|P()%2(?olGg@ypoJwp6>m z*dnfjjWC>?_1p;%1brqZyDRR;8EntVA92EJ3ByOxj6a+bhPl z;a?m4rQAV1@QU^#M1HX)0+}A<7TCO`ZR_RzF}X9-M>cRLyN4C+lCk2)kT^3gN^`IT zNP~fAm(wyIoR+l^lQDA(e1Yv}&$I!n?&*p6?lZcQ+vGLLd~fM)qt}wsbf3r=tmVYe zl)ntf#E!P7wlakP9MXS7m0nsAmqxZ*)#j;M&0De`oNmFgi$ov#!`6^4)iQyxg5Iuj zjLAhzQ)r`^hf7`*1`Rh`X;LVBtDSz@0T?kkT1o!ijeyTGt5vc^Cd*tmNgiNo^EaWvaC8$e+nb_{W01j3%=1Y&92YacjCi>eNbwk%-gPQ@H-+4xskQ}f_c=jg^S-# zYFBDf)2?@5cy@^@FHK5$YdAK9cI;!?Jgd}25lOW%xbCJ>By3=HiK@1EM+I46A)Lsd zeT|ZH;KlCml=@;5+hfYf>QNOr^XNH%J-lvev)$Omy8MZ`!{`j>(J5cG&ZXXgv)TaF zg;cz99i$4CX_@3MIb?GL0s*8J=3`#P(jXF(_(6DXZjc@(@h&=M&JG)9&Te1?(^XMW zjjC_70|b=9hB6pKQi`S^Ls7JyJw^@P>Ko^&q8F&?>6i;#CbxUiLz1ZH4lNyd@QACd zu>{!sqjB!2Dg}pbAXD>d!3jW}=5aN0b;rw*W>*PAxm7D)aw(c*RX2@bTGEI|RRp}vw7;NR2wa;rXN{L{Q#=Fa z$x@ms6pqb>!8AuV(prv>|aU8oWV={C&$c zMa=p=CDNOC2tISZcd8~18GN5oTbKY+Vrq;3_obJlfSKRMk;Hdp1`y`&LNSOqeauR_ z^j*Ojl3Ohzb5-a49A8s|UnM*NM8tg}BJXdci5%h&;$afbmRpN0&~9rCnBA`#lG!p zc{(9Y?A0Y9yo?wSYn>iigf~KP$0*@bGZ>*YM4&D;@{<%Gg5^uUJGRrV4 z(aZOGB&{_0f*O=Oi0k{@8vN^BU>s3jJRS&CJOl3o|BE{FAA&a#2YYiX3pZz@|Go-F z|Fly;7eX2OTs>R}<`4RwpHFs9nwh)B28*o5qK1Ge=_^w0m`uJOv!=&!tzt#Save(C zgKU=Bsgql|`ui(e1KVxR`?>Dx>(rD1$iWp&m`v)3A!j5(6vBm*z|aKm*T*)mo(W;R zNGo2`KM!^SS7+*9YxTm6YMm_oSrLceqN*nDOAtagULuZl5Q<7mOnB@Hq&P|#9y{5B z!2x+2s<%Cv2Aa0+u{bjZXS);#IFPk(Ph-K7K?3i|4ro> zRbqJoiOEYo(Im^((r}U4b8nvo_>4<`)ut`24?ILnglT;Pd&U}$lV3U$F9#PD(O=yV zgNNA=GW|(E=&m_1;uaNmipQe?pon4{T=zK!N!2_CJL0E*R^XXIKf*wi!>@l}3_P9Z zF~JyMbW!+n-+>!u=A1ESxzkJy$DRuG+$oioG7(@Et|xVbJ#BCt;J43Nvj@MKvTxzy zMmjNuc#LXBxFAwIGZJk~^!q$*`FME}yKE8d1f5Mp}KHNq(@=Z8YxV}0@;YS~|SpGg$_jG7>_8WWYcVx#4SxpzlV9N4aO>K{c z$P?a_fyDzGX$Of3@ykvedGd<@-R;M^Shlj*SswJLD+j@hi_&_>6WZ}#AYLR0iWMK|A zH_NBeu(tMyG=6VO-=Pb>-Q#$F*or}KmEGg*-n?vWQREURdB#+6AvOj*I%!R-4E_2$ zU5n9m>RWs|Wr;h2DaO&mFBdDb-Z{APGQx$(L`if?C|njd*fC=rTS%{o69U|meRvu?N;Z|Y zbT|ojL>j;q*?xXmnHH#3R4O-59NV1j=uapkK7}6@Wo*^Nd#(;$iuGsb;H315xh3pl zHaJ>h-_$hdNl{+|Zb%DZH%ES;*P*v0#}g|vrKm9;j-9e1M4qX@zkl&5OiwnCz=tb6 zz<6HXD+rGIVpGtkb{Q^LIgExOm zz?I|oO9)!BOLW#krLmWvX5(k!h{i>ots*EhpvAE;06K|u_c~y{#b|UxQ*O@Ks=bca z^_F0a@61j3I(Ziv{xLb8AXQj3;R{f_l6a#H5ukg5rxwF9A$?Qp-Mo54`N-SKc}fWp z0T)-L@V$$&my;l#Ha{O@!fK4-FSA)L&3<${Hcwa7ue`=f&YsXY(NgeDU#sRlT3+9J z6;(^(sjSK@3?oMo$%L-nqy*E;3pb0nZLx6 z;h5)T$y8GXK1DS-F@bGun8|J(v-9o=42&nLJy#}M5D0T^5VWBNn$RpC zZzG6Bt66VY4_?W=PX$DMpKAI!d`INr) zkMB{XPQ<52rvWVQqgI0OL_NWxoe`xxw&X8yVftdODPj5|t}S6*VMqN$-h9)1MBe0N zYq?g0+e8fJCoAksr0af1)FYtz?Me!Cxn`gUx&|T;)695GG6HF7!Kg1zzRf_{VWv^bo81v4$?F6u2g|wxHc6eJQAg&V z#%0DnWm2Rmu71rPJ8#xFUNFC*V{+N_qqFH@gYRLZ6C?GAcVRi>^n3zQxORPG)$-B~ z%_oB?-%Zf7d*Fe;cf%tQwcGv2S?rD$Z&>QC2X^vwYjnr5pa5u#38cHCt4G3|efuci z@3z=#A13`+ztmp;%zjXwPY_aq-;isu*hecWWX_=Z8paSqq7;XYnUjK*T>c4~PR4W7 z#C*%_H&tfGx`Y$w7`dXvVhmovDnT>btmy~SLf>>~84jkoQ%cv=MMb+a{JV&t0+1`I z32g_Y@yDhKe|K^PevP~MiiVl{Ou7^Mt9{lOnXEQ`xY^6L8D$705GON{!1?1&YJEl#fTf5Z)da=yiEQ zGgtC-soFGOEBEB~ZF_{7b(76En>d}mI~XIwNw{e>=Fv)sgcw@qOsykWr?+qAOZSVrQfg}TNI ztKNG)1SRrAt6#Q?(me%)>&A_^DM`pL>J{2xu>xa$3d@90xR61TQDl@fu%_85DuUUA za9tn64?At;{`BAW6oykwntxHeDpXsV#{tmt5RqdN7LtcF4vR~_kZNT|wqyR#z^Xcd zFdymVRZvyLfTpBT>w9<)Ozv@;Yk@dOSVWbbtm^y@@C>?flP^EgQPAwsy75bveo=}T zFxl(f)s)j(0#N_>Or(xEuV(n$M+`#;Pc$1@OjXEJZumkaekVqgP_i}p`oTx;terTx zZpT+0dpUya2hqlf`SpXN{}>PfhajNk_J0`H|2<5E;U5Vh4F8er z;RxLSFgpGhkU>W?IwdW~NZTyOBrQ84H7_?gviIf71l`EETodG9a1!8e{jW?DpwjL? zGEM&eCzwoZt^P*8KHZ$B<%{I}>46IT%jJ3AnnB5P%D2E2Z_ z1M!vr#8r}1|KTqWA4%67ZdbMW2YJ81b(KF&SQ2L1Qn(y-=J${p?xLMx3W7*MK;LFQ z6Z`aU;;mTL4XrrE;HY*Rkh6N%?qviUGNAKiCB~!P}Z->IpO6E(gGd7I#eDuT7j|?nZ zK}I(EJ>$Kb&@338M~O+em9(L!+=0zBR;JAQesx|3?Ok90)D1aS9P?yTh6Poh8Cr4X zk3zc=f2rE7jj+aP7nUsr@~?^EGP>Q>h#NHS?F{Cn`g-gD<8F&dqOh-0sa%pfL`b+1 zUsF*4a~)KGb4te&K0}bE>z3yb8% zibb5Q%Sfiv7feb1r0tfmiMv z@^4XYwg@KZI=;`wC)`1jUA9Kv{HKe2t$WmRcR4y8)VAFjRi zaz&O7Y2tDmc5+SX(bj6yGHYk$dBkWc96u3u&F)2yEE~*i0F%t9Kg^L6MJSb&?wrXi zGSc;_rln$!^ybwYBeacEFRsVGq-&4uC{F)*Y;<0y7~USXswMo>j4?~5%Zm!m@i@-> zXzi82sa-vpU{6MFRktJy+E0j#w`f`>Lbog{zP|9~hg(r{RCa!uGe>Yl536cn$;ouH za#@8XMvS-kddc1`!1LVq;h57~zV`7IYR}pp3u!JtE6Q67 zq3H9ZUcWPm2V4IukS}MCHSdF0qg2@~ufNx9+VMjQP&exiG_u9TZAeAEj*jw($G)zL zq9%#v{wVyOAC4A~AF=dPX|M}MZV)s(qI9@aIK?Pe+~ch|>QYb+78lDF*Nxz2-vpRbtQ*F4$0fDbvNM#CCatgQ@z1+EZWrt z2dZfywXkiW=no5jus-92>gXn5rFQ-COvKyegmL=4+NPzw6o@a?wGE-1Bt;pCHe;34K%Z z-FnOb%!nH;)gX+!a3nCk?5(f1HaWZBMmmC@lc({dUah+E;NOros{?ui1zPC-Q0);w zEbJmdE$oU$AVGQPdm{?xxI_0CKNG$LbY*i?YRQ$(&;NiA#h@DCxC(U@AJ$Yt}}^xt-EC_ z4!;QlLkjvSOhdx!bR~W|Ezmuf6A#@T`2tsjkr>TvW*lFCMY>Na_v8+{Y|=MCu1P8y z89vPiH5+CKcG-5lzk0oY>~aJC_0+4rS@c@ZVKLAp`G-sJB$$)^4*A!B zmcf}lIw|VxV9NSoJ8Ag3CwN&d7`|@>&B|l9G8tXT^BDHOUPrtC70NgwN4${$k~d_4 zJ@eo6%YQnOgq$th?0{h`KnqYa$Nz@vlHw<%!C5du6<*j1nwquk=uY}B8r7f|lY+v7 zm|JU$US08ugor8E$h3wH$c&i~;guC|3-tqJy#T;v(g( zBZtPMSyv%jzf->435yM(-UfyHq_D=6;ouL4!ZoD+xI5uCM5ay2m)RPmm$I}h>()hS zO!0gzMxc`BPkUZ)WXaXam%1;)gedA7SM8~8yIy@6TPg!hR0=T>4$Zxd)j&P-pXeSF z9W`lg6@~YDhd19B9ETv(%er^Xp8Yj@AuFVR_8t*KS;6VHkEDKI#!@l!l3v6`W1`1~ zP{C@keuV4Q`Rjc08lx?zmT$e$!3esc9&$XZf4nRL(Z*@keUbk!GZi(2Bmyq*saOD? z3Q$V<*P-X1p2}aQmuMw9nSMbOzuASsxten7DKd6A@ftZ=NhJ(0IM|Jr<91uAul4JR zADqY^AOVT3a(NIxg|U;fyc#ZnSzw2cr}#a5lZ38>nP{05D)7~ad7JPhw!LqOwATXtRhK!w0X4HgS1i<%AxbFmGJx9?sEURV+S{k~g zGYF$IWSlQonq6}e;B(X(sIH|;52+(LYW}v_gBcp|x%rEAVB`5LXg_d5{Q5tMDu0_2 z|LOm$@K2?lrLNF=mr%YP|U-t)~9bqd+wHb4KuPmNK<}PK6e@aosGZK57=Zt+kcszVOSbe;`E^dN! ze7`ha3WUUU7(nS0{?@!}{0+-VO4A{7+nL~UOPW9_P(6^GL0h${SLtqG!} zKl~Ng5#@Sy?65wk9z*3SA`Dpd4b4T^@C8Fhd8O)k_4%0RZL5?#b~jmgU+0|DB%0Z) zql-cPC>A9HPjdOTpPC` zQwvF}uB5kG$Xr4XnaH#ruSjM*xG?_hT7y3G+8Ox`flzU^QIgb_>2&-f+XB6MDr-na zSi#S+c!ToK84<&m6sCiGTd^8pNdXo+$3^l3FL_E`0 z>8it5YIDxtTp2Tm(?}FX^w{fbfgh7>^8mtvN>9fWgFN_*a1P`Gz*dyOZF{OV7BC#j zQV=FQM5m>47xXgapI$WbPM5V`V<7J9tD)oz@d~MDoM`R^Y6-Na(lO~uvZlpu?;zw6 zVO1faor3dg#JEb5Q*gz4<W8tgC3nE2BG2jeIQs1)<{In&7hJ39x=;ih;CJDy)>0S1at*7n?Wr0ahYCpFjZ|@u91Zl7( zv;CSBRC65-6f+*JPf4p1UZ)k=XivKTX6_bWT~7V#rq0Xjas6hMO!HJN8GdpBKg_$B zwDHJF6;z?h<;GXFZan8W{XFNPpOj!(&I1`&kWO86p?Xz`a$`7qV7Xqev|7nn_lQuX ziGpU1MMYt&5dE2A62iX3;*0WzNB9*nSTzI%62A+N?f?;S>N@8M=|ef3gtQTIA*=yq zQAAjOqa!CkHOQo4?TsqrrsJLclXcP?dlAVv?v`}YUjo1Htt;6djP@NPFH+&p1I+f_ z)Y279{7OWomY8baT(4TAOlz1OyD{4P?(DGv3XyJTA2IXe=kqD)^h(@*E3{I~w;ws8 z)ZWv7E)pbEM zd3MOXRH3mQhks9 zv6{s;k0y5vrcjXaVfw8^>YyPo=oIqd5IGI{)+TZq5Z5O&hXAw%ZlL}^6FugH;-%vP zAaKFtt3i^ag226=f0YjzdPn6|4(C2sC5wHFX{7QF!tG1E-JFA`>eZ`}$ymcRJK?0c zN363o{&ir)QySOFY0vcu6)kX#;l??|7o{HBDVJN+17rt|w3;(C_1b>d;g9Gp=8YVl zYTtA52@!7AUEkTm@P&h#eg+F*lR zQ7iotZTcMR1frJ0*V@Hw__~CL>_~2H2cCtuzYIUD24=Cv!1j6s{QS!v=PzwQ(a0HS zBKx04KA}-Ue+%9d`?PG*hIij@54RDSQpA7|>qYVIrK_G6%6;#ZkR}NjUgmGju)2F`>|WJoljo)DJgZr4eo1k1i1+o z1D{>^RlpIY8OUaOEf5EBu%a&~c5aWnqM zxBpJq98f=%M^{4mm~5`CWl%)nFR64U{(chmST&2jp+-r z3675V<;Qi-kJud%oWnCLdaU-)xTnMM%rx%Jw6v@=J|Ir=4n-1Z23r-EVf91CGMGNz zb~wyv4V{H-hkr3j3WbGnComiqmS0vn?n?5v2`Vi>{Ip3OZUEPN7N8XeUtF)Ry6>y> zvn0BTLCiqGroFu|m2zG-;Xb6;W`UyLw)@v}H&(M}XCEVXZQoWF=Ykr5lX3XWwyNyF z#jHv)A*L~2BZ4lX?AlN3X#axMwOC)PoVy^6lCGse9bkGjb=qz%kDa6}MOmSwK`cVO zt(e*MW-x}XtU?GY5}9{MKhRhYOlLhJE5=ca+-RmO04^ z66z{40J=s=ey9OCdc(RCzy zd7Zr1%!y3}MG(D=wM_ebhXnJ@MLi7cImDkhm0y{d-Vm81j`0mbi4lF=eirlr)oW~a zCd?26&j^m4AeXEsIUXiTal)+SPM4)HX%%YWF1?(FV47BaA`h9m67S9x>hWMVHx~Hg z1meUYoLL(p@b3?x|9DgWeI|AJ`Ia84*P{Mb%H$ZRROouR4wZhOPX15=KiBMHl!^JnCt$Az`KiH^_d>cev&f zaG2>cWf$=A@&GP~DubsgYb|L~o)cn5h%2`i^!2)bzOTw2UR!>q5^r&2Vy}JaWFUQE04v>2;Z@ZPwXr?y&G(B^@&y zsd6kC=hHdKV>!NDLIj+3rgZJ|dF`%N$DNd;B)9BbiT9Ju^Wt%%u}SvfM^=|q-nxDG zuWCQG9e#~Q5cyf8@y76#kkR^}{c<_KnZ0QsZcAT|YLRo~&tU|N@BjxOuy`#>`X~Q< z?R?-Gsk$$!oo(BveQLlUrcL#eirhgBLh`qHEMg`+sR1`A=1QX7)ZLMRT+GBy?&mM8 zQG^z-!Oa&J-k7I(3_2#Q6Bg=NX<|@X&+YMIOzfEO2$6Mnh}YV!m!e^__{W@-CTprr zbdh3f=BeCD$gHwCrmwgM3LAv3!Mh$wM)~KWzp^w)Cu6roO7uUG5z*}i0_0j47}pK; ztN530`ScGatLOL06~zO)Qmuv`h!gq5l#wx(EliKe&rz-5qH(hb1*fB#B+q`9=jLp@ zOa2)>JTl7ovxMbrif`Xe9;+fqB1K#l=Dv!iT;xF zdkCvS>C5q|O;}ns3AgoE({Ua-zNT-9_5|P0iANmC6O76Sq_(AN?UeEQJ>#b54fi3k zFmh+P%b1x3^)0M;QxXLP!BZ^h|AhOde*{9A=f3|Xq*JAs^Y{eViF|=EBfS6L%k4ip zk+7M$gEKI3?bQg?H3zaE@;cyv9kv;cqK$VxQbFEsy^iM{XXW0@2|DOu$!-k zSFl}Y=jt-VaT>Cx*KQnHTyXt}f9XswFB9ibYh+k2J!ofO+nD?1iw@mwtrqI4_i?nE zhLkPp41ED62me}J<`3RN80#vjW;wt`pP?%oQ!oqy7`miL>d-35a=qotK$p{IzeSk# ze_$CFYp_zIkrPFVaW^s#U4xT1lI^A0IBe~Y<4uS%zSV=wcuLr%gQT=&5$&K*bwqx| zWzCMiz>7t^Et@9CRUm9E+@hy~sBpm9fri$sE1zgLU((1?Yg{N1Sars=DiW&~Zw=3I zi7y)&oTC?UWD2w97xQ&5vx zRXEBGeJ(I?Y}eR0_O{$~)bMJRTsNUPIfR!xU9PE7A>AMNr_wbrFK>&vVw=Y;RH zO$mlpmMsQ}-FQ2cSj7s7GpC+~^Q~dC?y>M}%!-3kq(F3hGWo9B-Gn02AwUgJ>Z-pKOaj zysJBQx{1>Va=*e@sLb2z&RmQ7ira;aBijM-xQ&cpR>X3wP^foXM~u1>sv9xOjzZpX z0K;EGouSYD~oQ&lAafj3~EaXfFShC+>VsRlEMa9cg9i zFxhCKO}K0ax6g4@DEA?dg{mo>s+~RPI^ybb^u--^nTF>**0l5R9pocwB?_K)BG_)S zyLb&k%XZhBVr7U$wlhMqwL)_r&&n%*N$}~qijbkfM|dIWP{MyLx}X&}ES?}7i;9bW zmTVK@zR)7kE2+L42Q`n4m0VVg5l5(W`SC9HsfrLZ=v%lpef=Gj)W59VTLe+Z$8T8i z4V%5+T0t8LnM&H>Rsm5C%qpWBFqgTwL{=_4mE{S3EnBXknM&u8n}A^IIM4$s3m(Rd z>zq=CP-!9p9es2C*)_hoL@tDYABn+o#*l;6@7;knWIyDrt5EuakO99S$}n((Fj4y} zD!VvuRzghcE{!s;jC*<_H$y6!6QpePo2A3ZbX*ZzRnQq*b%KK^NF^z96CHaWmzU@f z#j;y?X=UP&+YS3kZx7;{ zDA{9(wfz7GF`1A6iB6fnXu0?&d|^p|6)%3$aG0Uor~8o? z*e}u#qz7Ri?8Uxp4m_u{a@%bztvz-BzewR6bh*1Xp+G=tQGpcy|4V_&*aOqu|32CM zz3r*E8o8SNea2hYJpLQ-_}R&M9^%@AMx&`1H8aDx4j%-gE+baf2+9zI*+Pmt+v{39 zDZ3Ix_vPYSc;Y;yn68kW4CG>PE5RoaV0n@#eVmk?p$u&Fy&KDTy!f^Hy6&^-H*)#u zdrSCTJPJw?(hLf56%2;_3n|ujUSJOU8VPOTlDULwt0jS@j^t1WS z!n7dZIoT+|O9hFUUMbID4Ec$!cc($DuQWkocVRcYSikFeM&RZ=?BW)mG4?fh#)KVG zcJ!<=-8{&MdE)+}?C8s{k@l49I|Zwswy^ZN3;E!FKyglY~Aq?4m74P-0)sMTGXqd5(S<-(DjjM z&7dL-Mr8jhUCAG$5^mI<|%`;JI5FVUnNj!VO2?Jiqa|c2;4^n!R z`5KK0hyB*F4w%cJ@Un6GC{mY&r%g`OX|1w2$B7wxu97%<@~9>NlXYd9RMF2UM>(z0 zouu4*+u+1*k;+nFPk%ly!nuMBgH4sL5Z`@Rok&?Ef=JrTmvBAS1h?C0)ty5+yEFRz zY$G=coQtNmT@1O5uk#_MQM1&bPPnspy5#>=_7%WcEL*n$;sSAZcXxMpcXxLe;_mLA z5F_paad+bGZV*oh@8h0(|D2P!q# zTHjmiphJ=AazSeKQPkGOR-D8``LjzToyx{lfK-1CDD6M7?pMZOdLKFtjZaZMPk4}k zW)97Fh(Z+_Fqv(Q_CMH-YYi?fR5fBnz7KOt0*t^cxmDoIokc=+`o# zrud|^h_?KW=Gv%byo~(Ln@({?3gnd?DUf-j2J}|$Mk>mOB+1{ZQ8HgY#SA8END(Zw z3T+W)a&;OO54~m}ffemh^oZ!Vv;!O&yhL0~hs(p^(Yv=(3c+PzPXlS5W79Er8B1o* z`c`NyS{Zj_mKChj+q=w)B}K za*zzPhs?c^`EQ;keH{-OXdXJet1EsQ)7;{3eF!-t^4_Srg4(Ot7M*E~91gwnfhqaM zNR7dFaWm7MlDYWS*m}CH${o?+YgHiPC|4?X?`vV+ws&Hf1ZO-w@OGG^o4|`b{bLZj z&9l=aA-Y(L11!EvRjc3Zpxk7lc@yH1e$a}8$_-r$)5++`_eUr1+dTb@ zU~2P1HM#W8qiNN3b*=f+FfG1!rFxnNlGx{15}BTIHgxO>Cq4 z;#9H9YjH%>Z2frJDJ8=xq>Z@H%GxXosS@Z>cY9ppF+)e~t_hWXYlrO6)0p7NBMa`+ z^L>-#GTh;k_XnE)Cgy|0Dw;(c0* zSzW14ZXozu)|I@5mRFF1eO%JM=f~R1dkNpZM+Jh(?&Zje3NgM{2ezg1N`AQg5%+3Y z64PZ0rPq6;_)Pj-hyIOgH_Gh`1$j1!jhml7ksHA1`CH3FDKiHLz+~=^u@kUM{ilI5 z^FPiJ7mSrzBs9{HXi2{sFhl5AyqwUnU{sPcUD{3+l-ZHAQ)C;c$=g1bdoxeG(5N01 zZy=t8i{*w9m?Y>V;uE&Uy~iY{pY4AV3_N;RL_jT_QtLFx^KjcUy~q9KcLE3$QJ{!)@$@En{UGG7&}lc*5Kuc^780;7Bj;)X?1CSy*^^ zPP^M)Pr5R>mvp3_hmCtS?5;W^e@5BjE>Cs<`lHDxj<|gtOK4De?Sf0YuK5GX9G93i zMYB{8X|hw|T6HqCf7Cv&r8A$S@AcgG1cF&iJ5=%+x;3yB`!lQ}2Hr(DE8=LuNb~Vs z=FO&2pdc16nD$1QL7j+!U^XWTI?2qQKt3H8=beVTdHHa9=MiJ&tM1RRQ-=+vy!~iz zj3O{pyRhCQ+b(>jC*H)J)%Wq}p>;?@W*Eut@P&?VU+Sdw^4kE8lvX|6czf{l*~L;J zFm*V~UC;3oQY(ytD|D*%*uVrBB}BbAfjK&%S;z;7$w68(8PV_whC~yvkZmX)xD^s6 z{$1Q}q;99W?*YkD2*;)tRCS{q2s@JzlO~<8x9}X<0?hCD5vpydvOw#Z$2;$@cZkYrp83J0PsS~!CFtY%BP=yxG?<@#{7%2sy zOc&^FJxsUYN36kSY)d7W=*1-{7ghPAQAXwT7z+NlESlkUH&8ODlpc8iC*iQ^MAe(B z?*xO4i{zFz^G=^G#9MsLKIN64rRJykiuIVX5~0#vAyDWc9-=6BDNT_aggS2G{B>dD ze-B%d3b6iCfc5{@yz$>=@1kdK^tX9qh0=ocv@9$ai``a_ofxT=>X7_Y0`X}a^M?d# z%EG)4@`^Ej_=%0_J-{ga!gFtji_byY&Vk@T1c|ucNAr(JNr@)nCWj?QnCyvXg&?FW;S-VOmNL6^km_dqiVjJuIASVGSFEos@EVF7St$WE&Z%)`Q##+0 zjaZ=JI1G@0!?l|^+-ZrNd$WrHBi)DA0-Eke>dp=_XpV<%CO_Wf5kQx}5e<90dt>8k zAi00d0rQ821nA>B4JHN7U8Zz=0;9&U6LOTKOaC1FC8GgO&kc=_wHIOGycL@c*$`ce703t%>S}mvxEnD-V!;6c`2(p74V7D0No1Xxt`urE66$0(ThaAZ1YVG#QP$ zy~NN%kB*zhZ2Y!kjn826pw4bh)75*e!dse+2Db(;bN34Uq7bLpr47XTX{8UEeC?2i z*{$`3dP}32${8pF$!$2Vq^gY|#w+VA_|o(oWmQX8^iw#n_crb(K3{69*iU?<%C-%H zuKi)3M1BhJ@3VW>JA`M>L~5*_bxH@Euy@niFrI$82C1}fwR$p2E&ZYnu?jlS}u7W9AyfdXh2pM>78bIt3 z)JBh&XE@zA!kyCDfvZ1qN^np20c1u#%P6;6tU&dx0phT1l=(mw7`u!-0e=PxEjDds z9E}{E!7f9>jaCQhw)&2TtG-qiD)lD(4jQ!q{`x|8l&nmtHkdul# zy+CIF8lKbp9_w{;oR+jSLtTfE+B@tOd6h=QePP>rh4@~!8c;Hlg9m%%&?e`*Z?qz5-zLEWfi>`ord5uHF-s{^bexKAoMEV@9nU z^5nA{f{dW&g$)BAGfkq@r5D)jr%!Ven~Q58c!Kr;*Li#`4Bu_?BU0`Y`nVQGhNZk@ z!>Yr$+nB=`z#o2nR0)V3M7-eVLuY`z@6CT#OTUXKnxZn$fNLPv7w1y7eGE=Qv@Hey`n;`U=xEl|q@CCV^#l)s0ZfT+mUf z^(j5r4)L5i2jnHW4+!6Si3q_LdOLQi<^fu?6WdohIkn79=jf%Fs3JkeXwF(?_tcF? z?z#j6iXEd(wJy4|p6v?xNk-)iIf2oX5^^Y3q3ziw16p9C6B;{COXul%)`>nuUoM*q zzmr|NJ5n)+sF$!yH5zwp=iM1#ZR`O%L83tyog-qh1I z0%dcj{NUs?{myT~33H^(%0QOM>-$hGFeP;U$puxoJ>>o-%Lk*8X^rx1>j|LtH$*)>1C!Pv&gd16%`qw5LdOIUbkNhaBBTo}5iuE%K&ZV^ zAr_)kkeNKNYJRgjsR%vexa~&8qMrQYY}+RbZ)egRg9_$vkoyV|Nc&MH@8L)`&rpqd zXnVaI@~A;Z^c3+{x=xgdhnocA&OP6^rr@rTvCnhG6^tMox$ulw2U7NgUtW%|-5VeH z_qyd47}1?IbuKtqNbNx$HR`*+9o=8`%vM8&SIKbkX9&%TS++x z5|&6P<%=F$C?owUI`%uvUq^yW0>`>yz!|WjzsoB9dT;2Dx8iSuK%%_XPgy0dTD4kd zDXF@&O_vBVVKQq(9YTClUPM30Sk7B!v7nOyV`XC!BA;BIVwphh+c)?5VJ^(C;GoQ$ zvBxr7_p*k$T%I1ke}`U&)$uf}I_T~#3XTi53OX)PoXVgxEcLJgZG^i47U&>LY(l%_ z;9vVDEtuMCyu2fqZeez|RbbIE7@)UtJvgAcVwVZNLccswxm+*L&w`&t=ttT=sv6Aq z!HouSc-24Y9;0q$>jX<1DnnGmAsP))- z^F~o99gHZw`S&Aw7e4id6Lg7kMk-e)B~=tZ!kE7sGTOJ)8@q}np@j7&7Sy{2`D^FH zI7aX%06vKsfJ168QnCM2=l|i>{I{%@gcr>ExM0Dw{PX6ozEuqFYEt z087%MKC;wVsMV}kIiuu9Zz9~H!21d!;Cu#b;hMDIP7nw3xSX~#?5#SSjyyg+Y@xh| z%(~fv3`0j#5CA2D8!M2TrG=8{%>YFr(j)I0DYlcz(2~92?G*?DeuoadkcjmZszH5& zKI@Lis%;RPJ8mNsbrxH@?J8Y2LaVjUIhRUiO-oqjy<&{2X~*f|)YxnUc6OU&5iac= z*^0qwD~L%FKiPmlzi&~a*9sk2$u<7Al=_`Ox^o2*kEv?p`#G(p(&i|ot8}T;8KLk- zPVf_4A9R`5^e`Om2LV*cK59EshYXse&IoByj}4WZaBomoHAPKqxRKbPcD`lMBI)g- zeMRY{gFaUuecSD6q!+b5(?vAnf>c`Z(8@RJy%Ulf?W~xB1dFAjw?CjSn$ph>st5bc zUac1aD_m6{l|$#g_v6;=32(mwpveQDWhmjR7{|B=$oBhz`7_g7qNp)n20|^^op3 zSfTdWV#Q>cb{CMKlWk91^;mHap{mk)o?udk$^Q^^u@&jd zfZ;)saW6{e*yoL6#0}oVPb2!}r{pAUYtn4{P~ES9tTfC5hXZnM{HrC8^=Pof{G4%Bh#8 ze~?C9m*|fd8MK;{L^!+wMy>=f^8b&y?yr6KnTq28$pFMBW9Oy7!oV5z|VM$s-cZ{I|Xf@}-)1=$V&x7e;9v81eiTi4O5-vs?^5pCKy2l>q);!MA zS!}M48l$scB~+Umz}7NbwyTn=rqt@`YtuwiQSMvCMFk2$83k50Q>OK5&fe*xCddIm)3D0I6vBU<+!3=6?(OhkO|b4fE_-j zimOzyfBB_*7*p8AmZi~X2bgVhyPy>KyGLAnOpou~sx9)S9%r)5dE%ADs4v%fFybDa_w*0?+>PsEHTbhKK^G=pFz z@IxLTCROWiKy*)cV3y%0FwrDvf53Ob_XuA1#tHbyn%Ko!1D#sdhBo`;VC*e1YlhrC z?*y3rp86m#qI|qeo8)_xH*G4q@70aXN|SP+6MQ!fJQqo1kwO_v7zqvUfU=Gwx`CR@ zRFb*O8+54%_8tS(ADh}-hUJzE`s*8wLI>1c4b@$al)l}^%GuIXjzBK!EWFO8W`>F^ ze7y#qPS0NI7*aU)g$_ziF(1ft;2<}6Hfz10cR8P}67FD=+}MfhrpOkF3hFhQu;Q1y zu%=jJHTr;0;oC94Hi@LAF5quAQ(rJG(uo%BiRQ@8U;nhX)j0i?0SL2g-A*YeAqF>RVCBOTrn{0R27vu}_S zS>tX4!#&U4W;ikTE!eFH+PKw%p+B(MR2I%n#+m0{#?qRP_tR@zpgCb=4rcrL!F=;A zh%EIF8m6%JG+qb&mEfuFTLHSxUAZEvC-+kvZKyX~SA3Umt`k}}c!5dy?-sLIM{h@> z!2=C)@nx>`;c9DdwZ&zeUc(7t<21D7qBj!|1^Mp1eZ6)PuvHx+poKSDCSBMFF{bKy z;9*&EyKitD99N}%mK8431rvbT+^%|O|HV23{;RhmS{$5tf!bIPoH9RKps`-EtoW5h zo6H_!s)Dl}2gCeGF6>aZtah9iLuGd19^z0*OryPNt{70RvJSM<#Ox9?HxGg04}b^f zrVEPceD%)#0)v5$YDE?f`73bQ6TA6wV;b^x*u2Ofe|S}+q{s5gr&m~4qGd!wOu|cZ||#h_u=k*fB;R6&k?FoM+c&J;ISg70h!J7*xGus)ta4veTdW)S^@sU@ z4$OBS=a~@F*V0ECic;ht4@?Jw<9kpjBgHfr2FDPykCCz|v2)`JxTH55?b3IM={@DU z!^|9nVO-R#s{`VHypWyH0%cs;0GO3E;It6W@0gX6wZ%W|Dzz&O%m17pa19db(er}C zUId1a4#I+Ou8E1MU$g=zo%g7K(=0Pn$)Rk z<4T2u<0rD)*j+tcy2XvY+0 z0d2pqm4)4lDewsAGThQi{2Kc3&C=|OQF!vOd#WB_`4gG3@inh-4>BoL!&#ij8bw7? zqjFRDaQz!J-YGitV4}$*$hg`vv%N)@#UdzHFI2E<&_@0Uw@h_ZHf}7)G;_NUD3@18 zH5;EtugNT0*RXVK*by>WS>jaDDfe!A61Da=VpIK?mcp^W?!1S2oah^wowRnrYjl~`lgP-mv$?yb6{{S55CCu{R z$9;`dyf0Y>uM1=XSl_$01Lc1Iy68IosWN8Q9Op=~I(F<0+_kKfgC*JggjxNgK6 z-3gQm6;sm?J&;bYe&(dx4BEjvq}b`OT^RqF$J4enP1YkeBK#>l1@-K`ajbn05`0J?0daOtnzh@l3^=BkedW1EahZlRp;`j*CaT;-21&f2wU z+Nh-gc4I36Cw+;3UAc<%ySb`#+c@5y ze~en&bYV|kn?Cn|@fqmGxgfz}U!98$=drjAkMi`43I4R%&H0GKEgx-=7PF}y`+j>r zg&JF`jomnu2G{%QV~Gf_-1gx<3Ky=Md9Q3VnK=;;u0lyTBCuf^aUi?+1+`4lLE6ZK zT#(Bf`5rmr(tgTbIt?yA@y`(Ar=f>-aZ}T~>G32EM%XyFvhn&@PWCm#-<&ApLDCXT zD#(9m|V(OOo7PmE@`vD4$S5;+9IQm19dd zvMEU`)E1_F+0o0-z>YCWqg0u8ciIknU#{q02{~YX)gc_u;8;i233D66pf(IkTDxeN zL=4z2)?S$TV9=ORVr&AkZMl<4tTh(v;Ix1{`pPVqI3n2ci&4Dg+W|N8TBUfZ*WeLF zqCH_1Q0W&f9T$lx3CFJ$o@Lz$99 zW!G&@zFHxTaP!o#z^~xgF|(vrHz8R_r9eo;TX9}2ZyjslrtH=%6O)?1?cL&BT(Amp zTGFU1%%#xl&6sH-UIJk_PGk_McFn7=%yd6tAjm|lnmr8bE2le3I~L{0(ffo}TQjyo zHZZI{-}{E4ohYTlZaS$blB!h$Jq^Rf#(ch}@S+Ww&$b);8+>g84IJcLU%B-W?+IY& zslcZIR>+U4v3O9RFEW;8NpCM0w1ROG84=WpKxQ^R`{=0MZCubg3st z48AyJNEvyxn-jCPTlTwp4EKvyEwD3e%kpdY?^BH0!3n6Eb57_L%J1=a*3>|k68A}v zaW`*4YitylfD}ua8V)vb79)N_Ixw_mpp}yJGbNu+5YYOP9K-7nf*jA1#<^rb4#AcS zKg%zCI)7cotx}L&J8Bqo8O1b0q;B1J#B5N5Z$Zq=wX~nQFgUfAE{@u0+EnmK{1hg> zC{vMfFLD;L8b4L+B51&LCm|scVLPe6h02rws@kGv@R+#IqE8>Xn8i|vRq_Z`V;x6F zNeot$1Zsu`lLS92QlLWF54za6vOEKGYQMdX($0JN*cjG7HP&qZ#3+bEN$8O_PfeAb z0R5;=zXac2IZ?fxu59?Nka;1lKm|;0)6|#RxkD05P5qz;*AL@ig!+f=lW5^Jbag%2 z%9@iM0ph$WFlxS!`p31t92z~TB}P-*CS+1Oo_g;7`6k(Jyj8m8U|Q3Sh7o-Icp4kV zK}%qri5>?%IPfamXIZ8pXbm-#{ytiam<{a5A+3dVP^xz!Pvirsq7Btv?*d7eYgx7q zWFxrzb3-%^lDgMc=Vl7^={=VDEKabTG?VWqOngE`Kt7hs236QKidsoeeUQ_^FzsXjprCDd@pW25rNx#6x&L6ZEpoX9Ffzv@olnH3rGOSW( zG-D|cV0Q~qJ>-L}NIyT?T-+x+wU%;+_GY{>t(l9dI%Ximm+Kmwhee;FK$%{dnF;C% zFjM2&$W68Sz#d*wtfX?*WIOXwT;P6NUw}IHdk|)fw*YnGa0rHx#paG!m=Y6GkS4VX zX`T$4eW9k1W!=q8!(#8A9h67fw))k_G)Q9~Q1e3f`aV@kbcSv7!priDUN}gX(iXTy zr$|kU0Vn%*ylmyDCO&G0Z3g>%JeEPFAW!5*H2Ydl>39w3W+gEUjL&vrRs(xGP{(ze zy7EMWF14@Qh>X>st8_029||TP0>7SG9on_xxeR2Iam3G~Em$}aGsNt$iES9zFa<3W zxtOF*!G@=PhfHO!=9pVPXMUVi30WmkPoy$02w}&6A7mF)G6-`~EVq5CwD2`9Zu`kd)52``#V zNSb`9dG~8(dooi1*-aSMf!fun7Sc`-C$-E(3BoSC$2kKrVcI!&yC*+ff2+C-@!AT_ zsvlAIV+%bRDfd{R*TMF><1&_a%@yZ0G0lg2K;F>7b+7A6pv3-S7qWIgx+Z?dt8}|S z>Qbb6x(+^aoV7FQ!Ph8|RUA6vXWQH*1$GJC+wXLXizNIc9p2yLzw9 z0=MdQ!{NnOwIICJc8!+Jp!zG}**r#E!<}&Te&}|B4q;U57$+pQI^}{qj669zMMe_I z&z0uUCqG%YwtUc8HVN7?0GHpu=bL7&{C>hcd5d(iFV{I5c~jpX&!(a{yS*4MEoYXh z*X4|Y@RVfn;piRm-C%b@{0R;aXrjBtvx^HO;6(>i*RnoG0Rtcd25BT6edxTNOgUAOjn zJ2)l{ipj8IP$KID2}*#F=M%^n&=bA0tY98@+2I+7~A&T-tw%W#3GV>GTmkHaqftl)#+E zMU*P(Rjo>8%P@_@#UNq(_L{}j(&-@1iY0TRizhiATJrnvwSH0v>lYfCI2ex^><3$q znzZgpW0JlQx?JB#0^^s-Js1}}wKh6f>(e%NrMwS`Q(FhazkZb|uyB@d%_9)_xb$6T zS*#-Bn)9gmobhAtvBmL+9H-+0_0US?g6^TOvE8f3v=z3o%NcPjOaf{5EMRnn(_z8- z$|m0D$FTU zDy;21v-#0i)9%_bZ7eo6B9@Q@&XprR&oKl4m>zIj-fiRy4Dqy@VVVs?rscG| zmzaDQ%>AQTi<^vYCmv#KOTd@l7#2VIpsj?nm_WfRZzJako`^uU%Nt3e;cU*y*|$7W zLm%fX#i_*HoUXu!NI$ey>BA<5HQB=|nRAwK!$L#n-Qz;~`zACig0PhAq#^5QS<8L2 zS3A+8%vbVMa7LOtTEM?55apt(DcWh#L}R^P2AY*c8B}Cx=6OFAdMPj1f>k3#^#+Hk z6uW1WJW&RlBRh*1DLb7mJ+KO>!t^t8hX1#_Wk`gjDio9)9IGbyCAGI4DJ~orK+YRv znjxRMtshZQHc$#Y-<-JOV6g^Cr@odj&Xw5B(FmI)*qJ9NHmIz_r{t)TxyB`L-%q5l ztzHgD;S6cw?7Atg*6E1!c6*gPRCb%t7D%z<(xm+K{%EJNiI2N0l8ud0Ch@_av_RW? zIr!nO4dL5466WslE6MsfMss7<)-S!e)2@r2o=7_W)OO`~CwklRWzHTfpB)_HYwgz=BzLhgZ9S<{nLBOwOIgJU=94uj6r!m>Xyn9>&xP+=5!zG_*yEoRgM0`aYts z^)&8(>z5C-QQ*o_s(8E4*?AX#S^0)aqB)OTyX>4BMy8h(cHjA8ji1PRlox@jB*1n? zDIfyDjzeg91Ao(;Q;KE@zei$}>EnrF6I}q&Xd=~&$WdDsyH0H7fJX|E+O~%LS*7^Q zYzZ4`pBdY{b7u72gZm6^5~O-57HwzwAz{)NvVaowo`X02tL3PpgLjwA`^i9F^vSpN zAqH3mRjG8VeJNHZ(1{%!XqC+)Z%D}58Qel{_weSEHoygT9pN@i zi=G;!Vj6XQk2tuJC>lza%ywz|`f7TIz*EN2Gdt!s199Dr4Tfd_%~fu8gXo~|ogt5Q zlEy_CXEe^BgsYM^o@L?s33WM14}7^T(kqohOX_iN@U?u;$l|rAvn{rwy>!yfZw13U zB@X9)qt&4;(C6dP?yRsoTMI!j-f1KC!<%~i1}u7yLXYn)(#a;Z6~r>hp~kfP));mi zcG%kdaB9H)z9M=H!f>kM->fTjRVOELNwh1amgKQT=I8J66kI)u_?0@$$~5f`u%;zl zC?pkr^p2Fe=J~WK%4ItSzKA+QHqJ@~m|Cduv=Q&-P8I5rQ-#G@bYH}YJr zUS(~(w|vKyU(T(*py}jTUp%I%{2!W!K(i$uvotcPjVddW z8_5HKY!oBCwGZcs-q`4Yt`Zk~>K?mcxg51wkZlX5e#B08I75F7#dgn5yf&Hrp`*%$ zQ;_Qg>TYRzBe$x=T(@WI9SC!ReSas9vDm(yslQjBJZde5z8GDU``r|N(MHcxNopGr z_}u39W_zwWDL*XYYt>#Xo!9kL#97|EAGyGBcRXtLTd59x%m=3i zL^9joWYA)HfL15l9%H?q`$mY27!<9$7GH(kxb%MV>`}hR4a?+*LH6aR{dzrX@?6X4 z3e`9L;cjqYb`cJmophbm(OX0b)!AFG?5`c#zLagzMW~o)?-!@e80lvk!p#&CD8u5_r&wp4O0zQ>y!k5U$h_K;rWGk=U)zX!#@Q%|9g*A zWx)qS1?fq6X<$mQTB$#3g;;5tHOYuAh;YKSBz%il3Ui6fPRv#v62SsrCdMRTav)Sg zTq1WOu&@v$Ey;@^+_!)cf|w_X<@RC>!=~+A1-65O0bOFYiH-)abINwZvFB;hJjL_$ z(9iScmUdMp2O$WW!520Hd0Q^Yj?DK%YgJD^ez$Z^?@9@Ab-=KgW@n8nC&88)TDC+E zlJM)L3r+ZJfZW_T$;Imq*#2<(j+FIk8ls7)WJ6CjUu#r5PoXxQs4b)mZza<8=v{o)VlLRM<9yw^0En#tXAj`Sylxvki{<1DPe^ zhjHwx^;c8tb?Vr$6ZB;$Ff$+3(*oinbwpN-#F)bTsXq@Sm?43MC#jQ~`F|twI=7oC zH4TJtu#;ngRA|Y~w5N=UfMZi?s0%ZmKUFTAye&6Y*y-%c1oD3yQ%IF2q2385Zl+=> zfz=o`Bedy|U;oxbyb^rB9ixG{Gb-{h$U0hVe`J;{ql!s_OJ_>>eoQn(G6h7+b^P48 zG<=Wg2;xGD-+d@UMZ!c;0>#3nws$9kIDkK13IfloGT@s14AY>&>>^#>`PT7GV$2Hp zN<{bN*ztlZu_%W=&3+=#3bE(mka6VoHEs~0BjZ$+=0`a@R$iaW)6>wp2w)=v2@|2d z%?34!+iOc5S@;AAC4hELWLH56RGxo4jw8MDMU0Wk2k_G}=Vo(>eRFo(g3@HjG|`H3 zm8b*dK=moM*oB<)*A$M9!!5o~4U``e)wxavm@O_R(`P|u%9^LGi(_%IF<6o;NLp*0 zKsfZ0#24GT8(G`i4UvoMh$^;kOhl?`0yNiyrC#HJH=tqOH^T_d<2Z+ zeN>Y9Zn!X4*DMCK^o75Zk2621bdmV7Rx@AX^alBG4%~;G_vUoxhfhFRlR&+3WwF^T zaL)8xPq|wCZoNT^>3J0K?e{J-kl+hu2rZI>CUv#-z&u@`hjeb+bBZ>bcciQVZ{SbW zez04s9oFEgc8Z+Kp{XFX`MVf-s&w9*dx7wLen(_@y34}Qz@&`$2+osqfxz4&d}{Ql z*g1ag00Gu+$C`0avds{Q65BfGsu9`_`dML*rX~hyWIe$T>CsPRoLIr%MTk3pJ^2zH1qub1MBzPG}PO;Wmav9w%F7?%l=xIf#LlP`! z_Nw;xBQY9anH5-c8A4mME}?{iewjz(Sq-29r{fV;Fc>fv%0!W@(+{={Xl-sJ6aMoc z)9Q+$bchoTGTyWU_oI19!)bD=IG&OImfy;VxNXoIO2hYEfO~MkE#IXTK(~?Z&!ae! zl8z{D&2PC$Q*OBC(rS~-*-GHNJ6AC$@eve>LB@Iq;jbBZj`wk4|LGogE||Ie=M5g= z9d`uYQ1^Sr_q2wmZE>w2WG)!F%^KiqyaDtIAct?}D~JP4shTJy5Bg+-(EA8aXaxbd~BKMtTf2iQ69jD1o* zZF9*S3!v-TdqwK$%&?91Sh2=e63;X0Lci@n7y3XOu2ofyL9^-I767eHESAq{m+@*r zbVDx!FQ|AjT;!bYsXv8ilQjy~Chiu&HNhFXt3R_6kMC8~ChEFqG@MWu#1Q1#=~#ix zrkHpJre_?#r=N0wv`-7cHHqU`phJX2M_^{H0~{VP79Dv{6YP)oA1&TSfKPEPZn2)G z9o{U1huZBLL;Tp_0OYw@+9z(jkrwIGdUrOhKJUbwy?WBt zlIK)*K0lQCY0qZ!$%1?3A#-S70F#YyUnmJF*`xx?aH5;gE5pe-15w)EB#nuf6B*c~ z8Z25NtY%6Wlb)bUA$w%HKs5$!Z*W?YKV-lE0@w^{4vw;J>=rn?u!rv$&eM+rpU6rc=j9>N2Op+C{D^mospMCjF2ZGhe4eADA#skp2EA26%p3Ex9wHW8l&Y@HX z$Qv)mHM}4*@M*#*ll5^hE9M^=q~eyWEai*P;4z<9ZYy!SlNE5nlc7gm;M&Q zKhKE4d*%A>^m0R?{N}y|i6i^k>^n4(wzKvlQeHq{l&JuFD~sTsdhs`(?lFK@Q{pU~ zb!M3c@*3IwN1RUOVjY5>uT+s-2QLWY z4T2>fiSn>>Fob+%B868-v9D@AfWr#M8eM6w#eAlhc#zk6jkLxGBGk`E3$!A@*am!R zy>29&ptYK6>cvP`b!syNp)Q$0UOW|-O@)8!?94GOYF_}+zlW%fCEl|Tep_zx05g6q z>tp47e-&R*hSNe{6{H!mL?+j$c^TXT{C&@T-xIaesNCl05 z9SLb@q&mSb)I{VXMaiWa3PWj=Ed!>*GwUe;^|uk=Pz$njNnfFY^MM>E?zqhf6^{}0 zx&~~dA5#}1ig~7HvOQ#;d9JZBeEQ+}-~v$at`m!(ai z$w(H&mWCC~;PQ1$%iuz3`>dWeb3_p}X>L2LK%2l59Tyc}4m0>9A!8rhoU3m>i2+hl zx?*qs*c^j}+WPs>&v1%1Ko8_ivAGIn@QK7A`hDz-Emkcgv2@wTbYhkiwX2l=xz*XG zaiNg+j4F-I>9v+LjosI-QECrtKjp&0T@xIMKVr+&)gyb4@b3y?2CA?=ooN zT#;rU86WLh(e@#mF*rk(NV-qSIZyr z$6!ZUmzD)%yO-ot`rw3rp6?*_l*@Z*IB0xn4|BGPWHNc-1ZUnNSMWmDh=EzWJRP`) zl%d%J613oXzh5;VY^XWJi{lB`f#u+ThvtP7 zq(HK<4>tw(=yzSBWtYO}XI`S1pMBe3!jFxBHIuwJ(@%zdQFi1Q_hU2eDuHqXte7Ki zOV55H2D6u#4oTfr7|u*3p75KF&jaLEDpxk!4*bhPc%mpfj)Us3XIG3 zIKMX^s^1wt8YK7Ky^UOG=w!o5e7W-<&c|fw2{;Q11vm@J{)@N3-p1U>!0~sKWHaL= zWV(0}1IIyt1p%=_-Fe5Kfzc71wg}`RDDntVZv;4!=&XXF-$48jS0Sc;eDy@Sg;+{A zFStc{dXT}kcIjMXb4F7MbX~2%i;UrBxm%qmLKb|2=?uPr00-$MEUIGR5+JG2l2Nq` zkM{{1RO_R)+8oQ6x&-^kCj)W8Z}TJjS*Wm4>hf+4#VJP)OBaDF%3pms7DclusBUw} z{ND#!*I6h85g6DzNvdAmnwWY{&+!KZM4DGzeHI?MR@+~|su0{y-5-nICz_MIT_#FE zm<5f3zlaKq!XyvY3H`9s&T};z!cK}G%;~!rpzk9-6L}4Rg7vXtKFsl}@sT#U#7)x- z7UWue5sa$R>N&b{J61&gvKcKlozH*;OjoDR+elkh|4bJ!_3AZNMOu?n9&|L>OTD78 z^i->ah_Mqc|Ev)KNDzfu1P3grBIM#%`QZqj5W{qu(HocQhjyS;UINoP`{J+DvV?|1 z_sw6Yr3z6%e7JKVDY<$P=M)dbk@~Yw9|2!Cw!io3%j92wTD!c^e9Vj+7VqXo3>u#= zv#M{HHJ=e$X5vQ>>ML?E8#UlmvJgTnb73{PSPTf*0)mcj6C z{KsfUbDK|F$E(k;ER%8HMdDi`=BfpZzP3cl5yJHu;v^o2FkHNk;cXc17tL8T!CsYI zfeZ6sw@;8ia|mY_AXjCS?kUfxdjDB28)~Tz1dGE|{VfBS9`0m2!m1yG?hR})er^pl4c@9Aq+|}ZlDaHL)K$O| z%9Jp-imI-Id0|(d5{v~w6mx)tUKfbuVD`xNt04Mry%M+jXzE>4(TBsx#&=@wT2Vh) z1yeEY&~17>0%P(eHP0HB^|7C+WJxQBTG$uyOWY@iDloRIb-Cf!p<{WQHR!422#F34 zG`v|#CJ^G}y9U*7jgTlD{D&y$Iv{6&PYG>{Ixg$pGk?lWrE#PJ8KunQC@}^6OP!|< zS;}p3to{S|uZz%kKe|;A0bL0XxPB&Q{J(9PyX`+Kr`k~r2}yP^ND{8!v7Q1&vtk& z2Y}l@J@{|2`oA%sxvM9i0V+8IXrZ4;tey)d;LZI70Kbim<4=WoTPZy=Yd|34v#$Kh zx|#YJ8s`J>W&jt#GcMpx84w2Z3ur-rK7gf-p5cE)=w1R2*|0mj12hvapuUWM0b~dG zMg9p8FmAZI@i{q~0@QuY44&mMUNXd7z>U58shA3o`p5eVLpq>+{(<3->DWuSFVZwC zxd50Uz(w~LxC4}bgag#q#NNokK@yNc+Q|Ap!u>Ddy+df>v;j@I12CDNN9do+0^n8p zMQs7X#+FVF0C5muGfN{r0|Nkql%BQT|K(DDNdR2pzM=_ea5+GO|J67`05AV92t@4l z0Qno0078PIHdaQGHZ~Scw!dzgqjK~3B7kf>BcP__&lLyU(cu3B^uLo%{j|Mb0NR)tkeT7Hcwp4O# z)yzu>cvG(d9~0a^)eZ;;%3ksk@F&1eEBje~ zW+-_s)&RgiweQc!otF>4%vbXKaOU41{!hw?|2`Ld3I8$&#WOsq>EG)1ANb!{N4z9@ zsU!bPG-~-bqCeIDzo^Q;gnucB{tRzm{ZH^Orphm2U+REA!*<*J6YQV83@&xoDl%#wnl5qcBqCcAF-vX5{30}(oJrnSH z{RY85hylK2dMOh2%oO1J8%)0?8TOL%rS8)+CsDv}aQ>4D)Jv+DLK)9gI^n-T^$)Tc zFPUD75qJm!Y-KBqj;JP4dV4 z`X{lGmn<)1IGz330}s}Jrjtf{(lnuuNHe5(ezA(pYa=1|Ff-LhPFK8 zyJh_b{yzu0yll6ZkpRzRjezyYivjyjW7QwO;@6X`m;2Apn2EK2!~7S}-*=;5*7K$B z`x(=!^?zgj(-`&ApZJXI09aDLXaT@<;CH=?fBOY5d|b~wBA@@p^K#nxr`)?i?SqTupI_PJ(A3cx`z~9mX_*)>L F{|7XC?P&l2 literal 0 HcmV?d00001 diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..2282996 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Dec 19 15:10:51 CET 2019 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip diff --git a/android/gradlew b/android/gradlew new file mode 100644 index 0000000..cccdd3d --- /dev/null +++ b/android/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat new file mode 100644 index 0000000..e95643d --- /dev/null +++ b/android/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega From 1e94096032cc1d25d27fb227d2e58c6ac36f048d Mon Sep 17 00:00:00 2001 From: xaviraol Date: Thu, 19 Dec 2019 15:31:38 +0100 Subject: [PATCH 02/13] fixed package on top of all files --- android/.project | 17 + .../sense/rninputkit/RNInputKitPackage.java | 39 ++ .../nl/sense/rninputkit/data/Constants.java | 26 + .../sense/rninputkit/data/ProviderName.java | 9 + .../helper/BloodPressureConverter.java | 41 ++ .../helper/BundleJSONConverter.java | 186 ++++++ .../rninputkit/helper/DataConverter.java | 30 + .../rninputkit/helper/LoggerFileWriter.java | 102 +++ .../sense/rninputkit/helper/UtilHelper.java | 99 +++ .../rninputkit/helper/ValueConverter.java | 119 ++++ .../rninputkit/helper/WeightConverter.java | 39 ++ .../rninputkit/modules/HealthBridge.java | 615 ++++++++++++++++++ .../rninputkit/modules/LoggerBridge.java | 43 ++ .../health/HealthPermissionPromise.java | 30 + .../modules/health/event/Event.java | 136 ++++ .../modules/health/event/EventHandler.java | 201 ++++++ .../health/event/ShortCodeGenerator.java | 30 + .../service/EventHandlerTaskService.java | 113 ++++ .../service/NotificationHelper.java | 38 ++ .../service/ServiceNotificationCompat.java | 78 +++ .../activity/detector/ActivityHandler.java | 122 ++++ .../detector/ActivityMonitoringService.java | 225 +++++++ .../activity/detector/ActivityState.java | 45 ++ .../service/activity/detector/Constants.java | 13 + .../DetectedActivitiesIntentService.java | 100 +++ .../service/broadcasts/BootReceiver.java | 24 + .../service/scheduler/IScheduler.java | 9 + .../scheduler/JobSchedulerService.java | 119 ++++ .../service/scheduler/SchedulerCompat.java | 67 ++ .../service/scheduler/v14/AlarmCompat.java | 185 ++++++ .../service/scheduler/v14/AlarmReceiver.java | 16 + example/android/.project | 17 + 32 files changed, 2933 insertions(+) create mode 100644 android/.project create mode 100644 android/src/main/java/nl/sense/rninputkit/RNInputKitPackage.java create mode 100644 android/src/main/java/nl/sense/rninputkit/data/Constants.java create mode 100644 android/src/main/java/nl/sense/rninputkit/data/ProviderName.java create mode 100644 android/src/main/java/nl/sense/rninputkit/helper/BloodPressureConverter.java create mode 100644 android/src/main/java/nl/sense/rninputkit/helper/BundleJSONConverter.java create mode 100644 android/src/main/java/nl/sense/rninputkit/helper/DataConverter.java create mode 100644 android/src/main/java/nl/sense/rninputkit/helper/LoggerFileWriter.java create mode 100644 android/src/main/java/nl/sense/rninputkit/helper/UtilHelper.java create mode 100644 android/src/main/java/nl/sense/rninputkit/helper/ValueConverter.java create mode 100644 android/src/main/java/nl/sense/rninputkit/helper/WeightConverter.java create mode 100644 android/src/main/java/nl/sense/rninputkit/modules/HealthBridge.java create mode 100644 android/src/main/java/nl/sense/rninputkit/modules/LoggerBridge.java create mode 100644 android/src/main/java/nl/sense/rninputkit/modules/health/HealthPermissionPromise.java create mode 100644 android/src/main/java/nl/sense/rninputkit/modules/health/event/Event.java create mode 100644 android/src/main/java/nl/sense/rninputkit/modules/health/event/EventHandler.java create mode 100644 android/src/main/java/nl/sense/rninputkit/modules/health/event/ShortCodeGenerator.java create mode 100644 android/src/main/java/nl/sense/rninputkit/service/EventHandlerTaskService.java create mode 100644 android/src/main/java/nl/sense/rninputkit/service/NotificationHelper.java create mode 100644 android/src/main/java/nl/sense/rninputkit/service/ServiceNotificationCompat.java create mode 100644 android/src/main/java/nl/sense/rninputkit/service/activity/detector/ActivityHandler.java create mode 100644 android/src/main/java/nl/sense/rninputkit/service/activity/detector/ActivityMonitoringService.java create mode 100644 android/src/main/java/nl/sense/rninputkit/service/activity/detector/ActivityState.java create mode 100644 android/src/main/java/nl/sense/rninputkit/service/activity/detector/Constants.java create mode 100644 android/src/main/java/nl/sense/rninputkit/service/activity/detector/DetectedActivitiesIntentService.java create mode 100644 android/src/main/java/nl/sense/rninputkit/service/broadcasts/BootReceiver.java create mode 100644 android/src/main/java/nl/sense/rninputkit/service/scheduler/IScheduler.java create mode 100644 android/src/main/java/nl/sense/rninputkit/service/scheduler/JobSchedulerService.java create mode 100644 android/src/main/java/nl/sense/rninputkit/service/scheduler/SchedulerCompat.java create mode 100644 android/src/main/java/nl/sense/rninputkit/service/scheduler/v14/AlarmCompat.java create mode 100644 android/src/main/java/nl/sense/rninputkit/service/scheduler/v14/AlarmReceiver.java create mode 100644 example/android/.project diff --git a/android/.project b/android/.project new file mode 100644 index 0000000..0e0a1ba --- /dev/null +++ b/android/.project @@ -0,0 +1,17 @@ + + + android_ + Project android_ created by Buildship. + + + + + org.eclipse.buildship.core.gradleprojectbuilder + + + + + + org.eclipse.buildship.core.gradleprojectnature + + diff --git a/android/src/main/java/nl/sense/rninputkit/RNInputKitPackage.java b/android/src/main/java/nl/sense/rninputkit/RNInputKitPackage.java new file mode 100644 index 0000000..fe48889 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/RNInputKitPackage.java @@ -0,0 +1,39 @@ +package nl.sense.rninputkit; + +import com.erasmus.modules.HealthBridge; +import com.erasmus.modules.LoggerBridge; +import com.erasmus.modules.health.event.EventHandler; +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.JavaScriptModule; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.ViewManager; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Created by ahmadmuhsin on 5/24/17. + */ + +public class RNInputKitPackage implements ReactPackage { + + @Override + public List createNativeModules(ReactApplicationContext reactContext) { + List modules = new ArrayList<>(); + modules.add(new HealthBridge(reactContext)); + modules.add(new LoggerBridge(reactContext)); + modules.add(new EventHandler(reactContext)); + return modules; + } + + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } + + public List> createJSModules() { + return Collections.emptyList(); + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/data/Constants.java b/android/src/main/java/nl/sense/rninputkit/data/Constants.java new file mode 100644 index 0000000..d171389 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/data/Constants.java @@ -0,0 +1,26 @@ +package nl.sense.rninputkit.data; + +import java.util.EnumMap; + +/** + * Created by kurniaeliazar on 3/20/17. + */ + +public class Constants { + public enum EVENTS { + actionTrigger, requestSessionId, + inputKitUpdates, inputKitTracking + } + + public static final EnumMap JS_SUPPORTED_EVENTS = new EnumMap<>(EVENTS.class); + static { + JS_SUPPORTED_EVENTS.put(EVENTS.actionTrigger, "ACTION_DID_TRIGGER"); + JS_SUPPORTED_EVENTS.put(EVENTS.requestSessionId, "REQUEST_VALID_SESSION_ID"); + JS_SUPPORTED_EVENTS.put(EVENTS.inputKitUpdates, "inputKitUpdates"); + JS_SUPPORTED_EVENTS.put(EVENTS.inputKitTracking, "inputKitTracking"); + } + + /** Used by Input Kits */ + public static final int REQUEST_RESOLVE_ERROR = 999; + public static final int REQ_REQUIRED_PERMISSIONS = 101; +} diff --git a/android/src/main/java/nl/sense/rninputkit/data/ProviderName.java b/android/src/main/java/nl/sense/rninputkit/data/ProviderName.java new file mode 100644 index 0000000..0d3c8eb --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/data/ProviderName.java @@ -0,0 +1,9 @@ +package nl.sense.rninputkit.data; + +public class ProviderName { + private ProviderName() { } + + /** Available Health Provider */ + public static final String GOOGLE_FIT = "googleFit"; + // TODO: Add another health provider when it's needed +} diff --git a/android/src/main/java/nl/sense/rninputkit/helper/BloodPressureConverter.java b/android/src/main/java/nl/sense/rninputkit/helper/BloodPressureConverter.java new file mode 100644 index 0000000..77aa02c --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/helper/BloodPressureConverter.java @@ -0,0 +1,41 @@ +package nl.sense.rninputkit.helper; + +import androidx.annotation.Nullable; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; + +import java.util.List; + +import nl.sense_os.input_kit.entity.BloodPressure; + +/** + * Created by xedi on 10/16/17. + */ + +public class BloodPressureConverter extends DataConverter { + + public WritableArray toWritableMap(@Nullable List bloodPressures) { + WritableArray array = Arguments.createArray(); + if (bloodPressures == null || bloodPressures.isEmpty()) return array; + + for (BloodPressure bp : bloodPressures) { + array.pushMap(toWritableMap(bp)); + } + return array; + } + + private WritableMap toWritableMap(@Nullable BloodPressure bp) { + WritableMap map = Arguments.createMap(); + if (bp == null) return map; + + map.putMap("time", toWritableMap(bp.getTimeRecord())); + map.putInt("systolic", bp.getSystolic()); + map.putInt("diastolic", bp.getDiastolic()); + map.putDouble("mean", bp.getMean()); + map.putInt("pulse", bp.getPulse()); + map.putString("comment", bp.getComment()); + return map; + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/helper/BundleJSONConverter.java b/android/src/main/java/nl/sense/rninputkit/helper/BundleJSONConverter.java new file mode 100644 index 0000000..e2b2b26 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/helper/BundleJSONConverter.java @@ -0,0 +1,186 @@ +package nl.sense.rninputkit.helper; + +import android.os.Bundle; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + + +/** + * com.facebook.internal is solely for the use of other packages within the Facebook SDK for + * Android. Use of any of the classes in this package is unsupported, and they may be modified or + * removed without warning at any time. + * + * A helper class that can round trip between JSON and Bundle objects that contains the types: + * Boolean, Integer, Long, Double, String + * If other types are found, an IllegalArgumentException is thrown. + */ +public class BundleJSONConverter { + private static final Map, Setter> SETTERS = new HashMap<>(); + + static { + SETTERS.put(Boolean.class, new Setter() { + public void setOnBundle(Bundle bundle, String key, Object value) { + bundle.putBoolean(key, (Boolean) value); + } + + public void setOnJSON(JSONObject json, String key, Object value) throws JSONException { + json.put(key, value); + } + }); + SETTERS.put(Integer.class, new Setter() { + public void setOnBundle(Bundle bundle, String key, Object value) { + bundle.putInt(key, (Integer) value); + } + + public void setOnJSON(JSONObject json, String key, Object value) throws JSONException { + json.put(key, value); + } + }); + SETTERS.put(Long.class, new Setter() { + public void setOnBundle(Bundle bundle, String key, Object value) { + bundle.putLong(key, (Long) value); + } + + public void setOnJSON(JSONObject json, String key, Object value) throws JSONException { + json.put(key, value); + } + }); + SETTERS.put(Double.class, new Setter() { + public void setOnBundle(Bundle bundle, String key, Object value) { + bundle.putDouble(key, (Double) value); + } + + public void setOnJSON(JSONObject json, String key, Object value) throws JSONException { + json.put(key, value); + } + }); + SETTERS.put(String.class, new Setter() { + public void setOnBundle(Bundle bundle, String key, Object value) { + bundle.putString(key, (String) value); + } + + public void setOnJSON(JSONObject json, String key, Object value) throws JSONException { + json.put(key, value); + } + }); + SETTERS.put(String[].class, new Setter() { + public void setOnBundle(Bundle bundle, String key, Object value) { + throw new IllegalArgumentException("Unexpected type from JSON"); + } + + public void setOnJSON(JSONObject json, String key, Object value) throws JSONException { + JSONArray jsonArray = new JSONArray(); + for (String stringValue : (String[]) value) { + jsonArray.put(stringValue); + } + json.put(key, jsonArray); + } + }); + + SETTERS.put(JSONArray.class, new Setter() { + public void setOnBundle(Bundle bundle, String key, Object value) throws JSONException { + JSONArray jsonArray = (JSONArray) value; + ArrayList stringArrayList = new ArrayList(); + // Empty list, can't even figure out the type, assume an ArrayList + if (jsonArray.length() == 0) { + bundle.putStringArrayList(key, stringArrayList); + return; + } + + // Only strings are supported for now + for (int i = 0; i < jsonArray.length(); i++) { + Object current = jsonArray.get(i); + if (current instanceof String) { + stringArrayList.add((String) current); + } else { + throw new IllegalArgumentException("Unexpected type in an array: " + current.getClass()); + } + } + bundle.putStringArrayList(key, stringArrayList); + } + + @Override + public void setOnJSON(JSONObject json, String key, Object value) throws JSONException { + throw new IllegalArgumentException("JSONArray's are not supported in bundles."); + } + }); + } + + public interface Setter { + void setOnBundle(Bundle bundle, String key, Object value) throws JSONException; + void setOnJSON(JSONObject json, String key, Object value) throws JSONException; + } + + public static JSONObject convertToJSON(Bundle bundle) throws JSONException { + JSONObject json = new JSONObject(); + + for (String key : bundle.keySet()) { + Object value = bundle.get(key); + if (value == null) { + // Null is not supported. + continue; + } + + // Special case List as getClass would not work, since List is an interface + if (value instanceof List) { + JSONArray jsonArray = new JSONArray(); + @SuppressWarnings("unchecked") + List listValue = (List) value; + for (String stringValue : listValue) { + jsonArray.put(stringValue); + } + json.put(key, jsonArray); + continue; + } + + // Special case Bundle as it's one way, on the return it will be JSONObject + if (value instanceof Bundle) { + json.put(key, convertToJSON((Bundle) value)); + continue; + } + + Setter setter = SETTERS.get(value.getClass()); + if (setter == null) { + throw new IllegalArgumentException("Unsupported type: " + value.getClass()); + } + setter.setOnJSON(json, key, value); + } + + return json; + } + + public static Bundle convertToBundle(JSONObject jsonObject) throws JSONException { + Bundle bundle = new Bundle(); + @SuppressWarnings("unchecked") + Iterator jsonIterator = jsonObject.keys(); + while (jsonIterator.hasNext()) { + String key = jsonIterator.next(); + Object value = jsonObject.get(key); + if (value == null || value == JSONObject.NULL) { + // Null is not supported. + continue; + } + + // Special case JSONObject as it's one way, on the return it would be Bundle. + if (value instanceof JSONObject) { + bundle.putBundle(key, convertToBundle((JSONObject) value)); + continue; + } + + Setter setter = SETTERS.get(value.getClass()); + if (setter == null) { + throw new IllegalArgumentException("Unsupported type: " + value.getClass()); + } + setter.setOnBundle(bundle, key, value); + } + + return bundle; + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/helper/DataConverter.java b/android/src/main/java/nl/sense/rninputkit/helper/DataConverter.java new file mode 100644 index 0000000..98b794f --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/helper/DataConverter.java @@ -0,0 +1,30 @@ +package nl.sense.rninputkit.helper; + +import androidx.annotation.Nullable; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableMap; + +import nl.sense_os.input_kit.entity.DateContent; + +/** + * Created by xedi on 10/13/17. + */ + +public class DataConverter { + private static final String EPOCH_PROPS = "timestamp"; + private static final String STRING_PROPS = "formattedString"; + /** + * Helper function to convert date content into writable map + * @param dateContent {@link DateContent} + * @return {@link WritableMap} + */ + protected WritableMap toWritableMap(@Nullable DateContent dateContent) { + WritableMap map = Arguments.createMap(); + if (dateContent == null) return map; + + map.putDouble(EPOCH_PROPS, dateContent.getEpoch()); + map.putString(STRING_PROPS, dateContent.getString()); + return map; + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/helper/LoggerFileWriter.java b/android/src/main/java/nl/sense/rninputkit/helper/LoggerFileWriter.java new file mode 100644 index 0000000..c855c99 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/helper/LoggerFileWriter.java @@ -0,0 +1,102 @@ +package nl.sense.rninputkit.helper; + +import android.content.Context; +import android.os.Environment; +import android.util.Log; + +import com.erasmus.BuildConfig; +import com.erasmus.R; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; + + +/** + * Created by panji on 22/02/18. + */ + +public class LoggerFileWriter { + private final Context context; + private static final String TAG = "LoggerFileWriter"; + private File logFile; + + public LoggerFileWriter(Context context) { + this.context = context; + if (BuildConfig.IS_DEBUG_MODE_ENABLED) { + initializeLogFile(); + } + } + + /** + * Log an event into file logger + * + * @param timeStamp Define a timestamp of recent event + * @param tag Define a tag of recent event + * @param message Define a message of recent event + * @throws IOException + */ + public void logEvent(long timeStamp, String tag, String message) { + if (BuildConfig.IS_DEBUG_MODE_ENABLED) { + try { + appendToLogFile(timeStamp + ": [" + tag + "]: " + message); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + /** + * Initialising log file on external storage + */ + private void initializeLogFile() { + File logFileDirectory = new File(Environment.getExternalStorageDirectory(), "sense"); + logFile = new File(logFileDirectory, String.format("%s-input-kit.log.txt", + context.getString(R.string.app_name))); + if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + Log.d(TAG, "initializeLogFile: Storage unavailable (probably mounted elsewhere)"); + return; + } + if (!logFileDirectory.exists() && !logFileDirectory.mkdirs()) { + Log.d(TAG, "initializeLogFile: Could not create the directory for log file"); + return; + } + if (!logFile.exists()) { + try { + createLogFile(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + /** + * Append an information into log file + * + * @param line Define a message that we want to append to the log file + * @throws IOException whenever something went wrong during writing to the log file + */ + private void appendToLogFile(String line) throws IOException { + Writer writer = null; + try { + writer = new OutputStreamWriter(new FileOutputStream(logFile, true), "UTF-8"); + writer.append(line).append("\n"); + writer.flush(); + } finally { + if (writer != null) writer.close(); + } + } + + /** + * Create new log file if it doesn't exists on local storage + * + * @throws IOException whenever something went wrong during creating a new log file + */ + private void createLogFile() throws IOException { + if (!logFile.createNewFile()) { + Log.d(TAG, "createLogFile: Could not create log file for writing"); + } + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/helper/UtilHelper.java b/android/src/main/java/nl/sense/rninputkit/helper/UtilHelper.java new file mode 100644 index 0000000..bdf84e0 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/helper/UtilHelper.java @@ -0,0 +1,99 @@ +package nl.sense.rninputkit.helper; + +import androidx.annotation.Nullable; + +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableMapKeySetIterator; +import com.facebook.react.bridge.ReadableType; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Created by kurniaeliazar on 3/22/17. + */ + +public class UtilHelper { + /** + * Converts a react native readable map into a JSON object. + * + * @param readableMap map to convert to JSON Object + * @return JSON Object that contains the readable map properties + */ + @Nullable + public static JSONObject readableMapToJson(ReadableMap readableMap) { + JSONObject jsonObject = new JSONObject(); + + if (readableMap == null) { + return null; + } + + ReadableMapKeySetIterator iterator = readableMap.keySetIterator(); + if (!iterator.hasNextKey()) { + return null; + } + + while (iterator.hasNextKey()) { + String key = iterator.nextKey(); + ReadableType readableType = readableMap.getType(key); + + try { + switch (readableType) { + case Null: + jsonObject.put(key, null); + break; + case Boolean: + jsonObject.put(key, readableMap.getBoolean(key)); + break; + case Number: + // Can be int or double. + jsonObject.put(key, readableMap.getDouble(key)); + break; + case String: + jsonObject.put(key, readableMap.getString(key)); + break; + case Map: + jsonObject.put(key, readableMapToJson(readableMap.getMap(key))); + break; + case Array: + jsonObject.put(key, convertArrayToJson(readableMap.getArray(key))); + default: + // Do nothing and fail silently + } + } catch (JSONException ex) { + // Do nothing and fail silently + } + } + + return jsonObject; + } + + public static JSONArray convertArrayToJson(ReadableArray readableArray) throws JSONException { + JSONArray array = new JSONArray(); + for (int i = 0; i < readableArray.size(); i++) { + switch (readableArray.getType(i)) { + case Boolean: + array.put(readableArray.getBoolean(i)); + break; + case Number: + array.put(readableArray.getDouble(i)); + break; + case String: + array.put(readableArray.getString(i)); + break; + case Map: + array.put(readableMapToJson(readableArray.getMap(i))); + break; + case Array: + array.put(convertArrayToJson(readableArray.getArray(i))); + break; + case Null: + default: + break; + } + } + return array; + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/helper/ValueConverter.java b/android/src/main/java/nl/sense/rninputkit/helper/ValueConverter.java new file mode 100644 index 0000000..6560134 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/helper/ValueConverter.java @@ -0,0 +1,119 @@ +package nl.sense.rninputkit.helper; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.util.List; + +import nl.sense_os.input_kit.entity.DateContent; +import nl.sense_os.input_kit.entity.IKValue; + +/** + * Created by panjiyudasetya on 10/23/17. + */ + +public class ValueConverter { + private ValueConverter() { } + private static final Gson GSON = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); + private static final String START_DATE_PROPS = "startDate"; + private static final String END_DATE_PROPS = "endDate"; + private static final String VALUE_PROPS = "value"; + private static final String EPOCH_PROPS = "timestamp"; + private static final String STRING_PROPS = "formattedString"; + + /** + * Helper function to convert detected value into writable map + * @param value Detected value + * @return {@link WritableMap} + */ + public static WritableMap toWritableMap(@Nullable IKValue value) { + WritableMap map = Arguments.createMap(); + if (value == null) return map; + + map.putMap(START_DATE_PROPS, toWritableMap(value.getStartDate())); + map.putMap(END_DATE_PROPS, toWritableMap(value.getEndDate())); + + Object objValue = value.getValue(); + if (objValue == null) { + map.putNull(VALUE_PROPS); + } else { + if (objValue instanceof Integer) { + map.putInt(VALUE_PROPS, (Integer) objValue); + } else if (objValue instanceof Double) { + map.putDouble(VALUE_PROPS, (Double) objValue); + } else if (objValue instanceof Float) { + map.putDouble(VALUE_PROPS, ((Float) objValue).doubleValue()); + } else if (objValue instanceof Long) { + map.putDouble(VALUE_PROPS, ((Long) objValue).doubleValue()); + } else if (objValue instanceof String) { + map.putString(VALUE_PROPS, (String) objValue); + } else if (objValue instanceof List) { + map = putListToMap((List) objValue, map); + } else { + map.putString(VALUE_PROPS, GSON.toJson(objValue)); + } + } + return map; + } + + /** + * Helper function to convert value list into {@link WritableArray} + * @param values Detected values + * @return {@link WritableArray} + */ + public static WritableArray toWritableArray(@Nullable List> values) { + WritableArray array = Arguments.createArray(); + if (values == null || values.size() == 0) return array; + + for (Object value : values) { + if (value instanceof IKValue) { + array.pushMap(toWritableMap((IKValue) value)); + } + } + return array; + } + + /** + * Helper function to put generic list to data into writable map + * @param list Generic object list + * @param map {@link WritableMap} target + */ + private static WritableMap putListToMap(@Nullable List list, + @NonNull WritableMap map) { + if (list == null || list.isEmpty()) { + map.putArray(VALUE_PROPS, Arguments.createArray()); + return map; + } + + WritableArray valueArray = Arguments.createArray(); + Object object = list.get(0); + if (object instanceof IKValue) { + for (Object value : list) { + valueArray.pushMap(toWritableMap((IKValue) value)); + } + map.putArray(VALUE_PROPS, valueArray); + } else map.putString(VALUE_PROPS, GSON.toJson(list)); + + return map; + } + + /** + * Helper function to convert date content into writable map + * @param dateContent {@link DateContent} + * @return {@link WritableMap} + */ + private static WritableMap toWritableMap(@Nullable DateContent dateContent) { + WritableMap map = Arguments.createMap(); + if (dateContent == null) return map; + + map.putDouble(EPOCH_PROPS, dateContent.getEpoch()); + map.putString(STRING_PROPS, dateContent.getString()); + return map; + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/helper/WeightConverter.java b/android/src/main/java/nl/sense/rninputkit/helper/WeightConverter.java new file mode 100644 index 0000000..5ccf3b7 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/helper/WeightConverter.java @@ -0,0 +1,39 @@ +package nl.sense.rninputkit.helper; + +import androidx.annotation.Nullable; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; + +import java.util.List; + +import nl.sense_os.input_kit.entity.Weight; + +/** + * Created by xedi on 10/13/17. + */ + +public class WeightConverter extends DataConverter { + + public WritableArray toWritableMap(@Nullable List weightList) { + WritableArray array = Arguments.createArray(); + if (weightList == null || weightList.isEmpty()) return array; + + for (Weight weight : weightList) { + array.pushMap(toWritableMap(weight)); + } + return array; + } + + private WritableMap toWritableMap(@Nullable Weight weight) { + WritableMap map = Arguments.createMap(); + if (weight == null) return map; + + map.putMap("time", toWritableMap(weight.getTimeRecorded())); + map.putDouble("weight", weight.getWeight()); + map.putInt("bodyFat", weight.getBodyFat()); + map.putString("comment", weight.getComment()); + return map; + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/modules/HealthBridge.java b/android/src/main/java/nl/sense/rninputkit/modules/HealthBridge.java new file mode 100644 index 0000000..e423e7b --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/modules/HealthBridge.java @@ -0,0 +1,615 @@ +package nl.sense.rninputkit.modules; + + +import android.app.Activity; +import android.content.Intent; +import androidx.annotation.NonNull; +import android.text.TextUtils; +import android.util.Log; +import android.util.Pair; + +import com.erasmus.data.Constants; +import com.erasmus.data.ProviderName; +import com.erasmus.helper.BloodPressureConverter; +import com.erasmus.helper.ValueConverter; +import com.erasmus.helper.WeightConverter; +import com.erasmus.modules.health.HealthPermissionPromise; +import com.erasmus.modules.health.event.EventHandler; +import com.erasmus.service.activity.detector.ActivityMonitoringService; +import com.facebook.react.bridge.ActivityEventListener; +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.LifecycleEventListener; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import nl.sense_os.input_kit.HealthProvider; +import nl.sense_os.input_kit.HealthProvider.ProviderType; +import nl.sense_os.input_kit.InputKit; +import nl.sense_os.input_kit.constant.IKStatus; +import nl.sense_os.input_kit.constant.SampleType; +import nl.sense_os.input_kit.entity.BloodPressure; +import nl.sense_os.input_kit.entity.IKValue; +import nl.sense_os.input_kit.entity.SensorDataPoint; +import nl.sense_os.input_kit.entity.StepContent; +import nl.sense_os.input_kit.entity.Weight; +import nl.sense_os.input_kit.googlefit.GoogleFitHealthProvider; +import nl.sense_os.input_kit.helper.AppHelper; +import nl.sense_os.input_kit.status.IKProviderInfo; +import nl.sense_os.input_kit.status.IKResultInfo; + +import static com.erasmus.data.Constants.JS_SUPPORTED_EVENTS; +import static nl.sense_os.input_kit.constant.IKStatus.Code.IK_NOT_CONNECTED; + +/** + * Created by panjiyudasetya on 5/30/17. + */ + +public class HealthBridge extends ReactContextBaseJavaModule implements ActivityEventListener, + LifecycleEventListener { + + private static final String HEALTH_FIT_MODULE_NAME = "HealthBridge"; + private static final String TAG = HEALTH_FIT_MODULE_NAME; + private ReactApplicationContext mReactContext; + private InputKit mInputKit; + private List mRequestHealthPromises; + private BloodPressureConverter mBloodPressureConverter; + private WeightConverter mWeightConverter; + private ProviderType mActiveProvider; + + @SuppressWarnings("unused") // Used by React Native + public HealthBridge(ReactApplicationContext reactContext) { + super(reactContext); + mReactContext = reactContext; + mReactContext.addLifecycleEventListener(this); + mReactContext.addActivityEventListener(this); + + mBloodPressureConverter = new BloodPressureConverter(); + mWeightConverter = new WeightConverter(); + + mRequestHealthPromises = new ArrayList<>(); + mActiveProvider = ProviderType.GOOGLE_FIT; + } + + @Override + public String getName() { + return HEALTH_FIT_MODULE_NAME; + } + + @Override + public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { + Log.d(TAG, "onActivityResult: Request Code : " + requestCode); + if (requestCode == GoogleFitHealthProvider.GF_PERMISSION_REQUEST_CODE) { + handlePromises(resultCode == Activity.RESULT_OK); + } + } + + @Override + public void onNewIntent(Intent intent) { + // At this point, we don't need to implement anything here + // since we only wants to maintain activity results from + // ConnectionResult#startResolutionForResult() + } + + /** + * Start monitoring health sensors. + * @param typeString Sensor type should be one of these {@link nl.sense_os.input_kit.constant.SampleType.SampleName} sensor + * @param promise contains an information of subscribing health sensor. + */ + @ReactMethod + @SuppressWarnings("unused")//Used by React Native Application + public void startMonitoring(final String typeString, final Promise promise) { + switch (mActiveProvider) { + case GOOGLE_FIT: + case SAMSUNG_HEALTH: + ActivityMonitoringService.subscribe(mReactContext); + promise.resolve(null); + break; + + default: + String notSupportedMsg = "Monitoring " + typeString + " is not supported."; + promise.reject(String.valueOf(IKStatus.Code.INVALID_REQUEST), notSupportedMsg); + break; + } + } + + /** + * Stop monitoring health sensors. + * @param typeString Sensor type should be one of these {@link nl.sense_os.input_kit.constant.SampleType.SampleName} sensor + * @param promise contains an information of unsubscribing health sensor. + */ + @ReactMethod + @SuppressWarnings("unused")//Used by React Native Application + public void stopMonitoring(final String typeString, final Promise promise) { + switch (mActiveProvider) { + case GOOGLE_FIT: + case SAMSUNG_HEALTH: + ActivityMonitoringService.unsubscribe(mReactContext); + promise.resolve(null); + break; + + default: + String notSupportedMsg = "Monitoring " + typeString + " is not supported."; + promise.reject(String.valueOf(IKStatus.Code.INVALID_REQUEST), notSupportedMsg); + break; + } + } + + /** + * Check Input Kit availability. + * @param promise contains an information whether successfully connect to Input Kit or not. + */ + @ReactMethod + @SuppressWarnings("unused")//Used by React Native application + public void isAvailable(final Promise promise) { + if (!mInputKit.isAvailable()) { + Log.d(TAG, "isAvailable: Make sure to call request permission before called this function"); + promise.reject(String.valueOf(IK_NOT_CONNECTED), IKStatus.INPUT_KIT_NOT_CONNECTED); + return; + } + + if (mInputKit.isAvailable()) promise.resolve(true); + } + + /** + * Check Health Provider installation. + * @param promise contains an information either health provider installed or not. + */ + @ReactMethod + @SuppressWarnings("unused")//Used by React Native application + public void isProviderInstalled(String providerName, Promise promise) { + // Make sure provider name is not null + if (TextUtils.isEmpty(providerName)) { + promise.reject( + String.valueOf(IKStatus.Code.INVALID_REQUEST), + "Provider name must be provided!" + ); + return; + } + + // TODO : Add another handler for supported health providers + if (providerName.equals(ProviderName.GOOGLE_FIT)) { + if (AppHelper.isGoogleFitInstalled(mReactContext)) { + promise.resolve(true); + } else { + promise.resolve(false); + } + return; + } + + promise.reject( + String.valueOf(IKStatus.Code.INVALID_REQUEST), + providerName + " is not supported in InputKit!" + ); + } + + /** + * Check whether permission has been authorised or not. + * @param permissions permission that needs to be checked + * @param promise resolved whenever permission has been authorised + * rejected if it hasn;t + */ + @ReactMethod + @SuppressWarnings("unused")//Used by React Native application + public void isPermissionsAuthorised(ReadableArray permissions, final Promise promise) { + if (!mInputKit.isPermissionsAuthorised(getConvertedPermission(permissions))) { + Log.d(TAG, "isAvailable: Make sure to call request permission before called this function"); + promise.reject(String.valueOf(IK_NOT_CONNECTED), IKStatus.INPUT_KIT_NOT_CONNECTED); + return; + } + + promise.resolve(true); + } + + /** + * Request all related permission for specific API. + * @param permissions containing an array of api permission.
+ * For example : ["sleep", "stepCount"] + * @param promise contains an information whether permissions successfully granted or not. + */ + @ReactMethod + @SuppressWarnings("unused")//Used by React Native application + public void requestPermissions(ReadableArray permissions, final Promise promise) { + mInputKit.authorize(new InputKit.Callback() { + @Override + public void onAvailable(String... addMessages) { + promise.resolve("CONNECTED_TO_INPUT_KIT"); + } + + @Override + public void onNotAvailable(@NonNull IKResultInfo reason) { + promise.reject(String.valueOf(reason.getResultCode()), reason.getMessage()); + } + + @Override + public void onConnectionRefused(@NonNull IKProviderInfo providerInfo) { + String message = providerInfo.getMessage(); + if (message.equals(IKStatus.INPUT_KIT_REQUIRED_GRANTED_PERMISSIONS)) { + mRequestHealthPromises.add(new HealthPermissionPromise(promise, providerInfo)); + } else { + promise.reject(String.valueOf( + providerInfo.getResultCode()), + message); + } + } + }, getConvertedPermission(permissions)); + } + + /** + * Get total distance of walk on specific time range. + * @param startTime epoch for the start date + * @param endTime epoch for the end date + * @param promise containing number of total distance in meters. + */ + @ReactMethod + @SuppressWarnings("unused")//Used by React Native application + public void getDistance(final Double startTime, + final Double endTime, + final Promise promise) { + Log.d(TAG, "getDistance: " + startTime + ", " + endTime); + mInputKit.getDistance( + startTime.longValue(), + endTime.longValue(), + 0, + new InputKit.Result() { + @Override + public void onNewData(Float data) { + Log.d(TAG, "getDistance#onNewData: " + data); + promise.resolve(Double.valueOf(data)); + } + + @Override + public void onError(@NonNull IKResultInfo error) { + promise.reject(String.valueOf(error.getResultCode()), error.getMessage()); + } + }); + } + + /** + * Get distance samples between start and end date (inclusive + overlapping) with the latest ones first and limit them by the limit count. + * The distance sample values returned are always in meters + * Specify a limit of 0 for unlimited samples + * @param startTime epoch for the start date + * @param endTime epoch for the end date + * @param promise containing number of total distance in meters. + * @param limit distance sample set limit + */ + @ReactMethod + @SuppressWarnings("unused")//Used by React Native application + public void getDistanceSamples(final Double startTime, + final Double endTime, + final Integer limit, + final Promise promise) { + Log.d(TAG, "getDistanceSamples: " + startTime + ", " + endTime + ", " + limit); + mInputKit.getDistanceSamples( + startTime.longValue(), + endTime.longValue(), + limit, + new InputKit.Result>>() { + @Override + public void onNewData(List> data) { + WritableArray objects = ValueConverter.toWritableArray(data); + Log.d(TAG, "getDistanceSample#onNewData: " + objects); + promise.resolve(objects); + } + + @Override + public void onError(@NonNull IKResultInfo error) { + promise.reject(String.valueOf(error.getResultCode()), error.getMessage()); + } + }); + } + + /** + * Get total steps count of specific range + * @param startTime epoch for the start date + * @param endTime epoch for the end date + * @param promise containing number of total steps count + */ + @ReactMethod + @SuppressWarnings("unused")//Used by React Native application + public void getStepCount(final Double startTime, + final Double endTime, + final Promise promise) { + Log.d(TAG, "getStepCount: " + startTime + ", " + endTime); + mInputKit.getStepCount( + startTime.longValue(), + endTime.longValue(), + 0, + new InputKit.Result() { + @Override + public void onNewData(Integer data) { + Log.d(TAG, "getStepCount#onNewData: " + data); + promise.resolve(data); + } + + @Override + public void onError(@NonNull IKResultInfo error) { + promise.reject(String.valueOf(error.getResultCode()), error.getMessage()); + } + }); + } + + /** + * Returns Promise contains distribution of step count value through out a specific range. + * + * @param startTime epoch for the start date of the range where the distribution should be calculated from. + * @param endTime epoch for the end date of the range where the distribution should be calculated from. + * @param interval Interval + * @param promise containing: + * value: array of data points + * startDate: start date + * endDate: end date + **/ + @ReactMethod + @SuppressWarnings("unused")//Used by React Native application + public void getStepCountDistribution(final Double startTime, + final Double endTime, + final String interval, + final Promise promise) { + Log.d(TAG, "getStepCountDistribution: " + startTime + ", " + endTime + ", " + interval); + mInputKit.getStepCountDistribution( + startTime.longValue(), + endTime.longValue(), + interval, + 0, + new InputKit.Result() { + @Override + public void onNewData(StepContent data) { + Log.d(TAG, "getStepCountDistribution#onNewData: " + data.toJson()); + WritableMap object = ValueConverter.toWritableMap(data); + Log.d(TAG, "getStepCountDistribution#onNewData: CONVERTED " + object); + promise.resolve(object); + } + + @Override + public void onError(@NonNull IKResultInfo error) { + promise.reject(String.valueOf(error.getResultCode()), error.getMessage()); + } + }); + } + + @ReactMethod + @SuppressWarnings("unused")//Used by React Native application + public void getSleepAnalysisSamples(final Double startTime, + final Double endTime, + final Promise promise) { + mInputKit.getSleepAnalysisSamples( + startTime.longValue(), + endTime.longValue(), + new InputKit.Result>>() { + @Override + public void onNewData(List> data) { + WritableArray object = ValueConverter.toWritableArray(data); + promise.resolve(object); + } + @Override + public void onError(@NonNull IKResultInfo error) { + promise.reject(String.valueOf(error.getResultCode()), error.getMessage()); + } + } + ); + } + + @ReactMethod + @SuppressWarnings("unused")//Used by React Native application + public void getWeightData(final Double startTime, + final Double endTime, + final Promise promise) { + mInputKit.getWeight( + startTime.longValue(), + endTime.longValue(), + new InputKit.Result>() { + @Override + public void onNewData(List data) { + WritableArray object = mWeightConverter.toWritableMap(data); + promise.resolve(object); + } + @Override + public void onError(@NonNull IKResultInfo error) { + promise.reject(String.valueOf(error.getResultCode()), error.getMessage()); + } + } + ); + } + + @ReactMethod + @SuppressWarnings("unused")//Used by React Native application + public void getBloodPressure(final Double startTime, + final Double endTime, + final Promise promise) { + mInputKit.getBloodPressure( + startTime.longValue(), + endTime.longValue(), + new InputKit.Result>() { + @Override + public void onNewData(List data) { + WritableArray object = mBloodPressureConverter.toWritableMap(data); + promise.resolve(object); + } + @Override + public void onError(@NonNull IKResultInfo error) { + promise.reject(String.valueOf(error.getResultCode()), error.getMessage()); + } + } + + ); + } + + /** + * Start tracking specific sensor. + * + * @param sampleType Sample type should be one of these {@link SampleType.SampleName} sensor + * @param startTime Start time of sensor tracking. Actually on Android is not necessary since + * it will use refresh rate + * @param promise Containing an information of request code and code message whether + * tracking action successfully or not + */ + @ReactMethod + @SuppressWarnings("unused")//Used by React Native application + public void startTracking(final String sampleType, + final Double startTime, + final Promise promise) { + mInputKit.startTracking( + sampleType, + Pair.create(1, TimeUnit.MINUTES), + new HealthProvider.SensorListener() { + @Override + public void onSubscribe(@NonNull IKResultInfo info) { + if (info.getResultCode() == IKStatus.Code.VALID_REQUEST) { + promise.resolve(info.getMessage()); + return; + } + promise.reject(String.valueOf(info.getResultCode()), info.getMessage()); + } + + @Override + public void onReceive(@NonNull SensorDataPoint data) { + if (mReactContext != null) { + EventHandler.emit( + mReactContext.getApplicationContext(), + JS_SUPPORTED_EVENTS.get(Constants.EVENTS.inputKitTracking), + data, + // TODO : Does completion callback is necessary? + new Callback() { + @Override + public void invoke(Object... args) { + + } + } + ); + } + } + + @Override + public void onUnsubscribe(@NonNull IKResultInfo info) { + promise.reject(String.valueOf(info.getResultCode()), info.getMessage()); + } + }); + } + + /** + * Stop tracking specific sensor. + * + * @param sampleType Sample type should be one of these {@link SampleType.SampleName} sensor + * @param promise Containing an information of request code and code message whether + * tracking action successfully or not + */ + @ReactMethod + @SuppressWarnings("unused")//Used by React Native application + public void stopTracking(final String sampleType, + final Promise promise) { + mInputKit.stopTracking( + sampleType, + new HealthProvider.SensorListener() { + @Override + public void onSubscribe(@NonNull IKResultInfo info) { } + + @Override + public void onReceive(@NonNull SensorDataPoint data) { } + + @Override + public void onUnsubscribe(@NonNull IKResultInfo info) { + if (info.getResultCode() == IKStatus.Code.VALID_REQUEST) { + promise.resolve(info.getMessage()); + return; + } + promise.reject(String.valueOf(info.getResultCode()), info.getMessage()); + } + }); + } + + /** + * Stop all tracking sensors. + * + * @param promise Containing an information of request code and code message whether + * tracking action successfully or not + */ + @ReactMethod + @SuppressWarnings("unused")//Used by React Native application + public void stopTrackingAll(final Promise promise) { + mInputKit.stopTrackingAll(new HealthProvider.SensorListener() { + @Override + public void onSubscribe(@NonNull IKResultInfo info) { } + + @Override + public void onReceive(@NonNull SensorDataPoint data) { } + + @Override + public void onUnsubscribe(@NonNull IKResultInfo info) { + if (info.getResultCode() == IKStatus.Code.VALID_REQUEST) { + promise.resolve(info.getMessage()); + return; + } + promise.reject(String.valueOf(info.getResultCode()), info.getMessage()); + } + }); + } + + @Override + public void onHostResume() { + // Do nothing here, as long as host module didn't destroyed, + // we still able to obtain sensor manager & subscribe listener + if (mInputKit == null) { + mInputKit = InputKit.getInstance(mReactContext); + mInputKit.setHealthProvider(mActiveProvider); + } + mInputKit.setHostActivity(getCurrentActivity()); + } + + @Override + public void onHostPause() { + // Do nothing here, as long as host module didn't destroyed, + // we still able to obtain sensor manager & subscribe listener + } + + @Override + public void onHostDestroy() { + // Do nothing here, as long as host module didn't destroyed, + // we still able to obtain sensor manager & subscribe listener + } + + private void handlePromises(boolean isResolved) { + String message = "CONNECTED_TO_INPUT_KIT"; + // Resolve promises, last in first out + for (int i = mRequestHealthPromises.size(); i > 0; i--) { + HealthPermissionPromise permissionPromise = mRequestHealthPromises.get(i - 1); + if (isResolved) { + permissionPromise + .getPromise() + .resolve(message); + } else { + IKProviderInfo info = permissionPromise.getProviderInfo(); + permissionPromise + .getPromise() + .reject(String.valueOf(info.getResultCode()), info.getMessage()); + } + } + mRequestHealthPromises.clear(); + } + + private String[] getConvertedPermission(ReadableArray permissionTypes) { + if (permissionTypes == null || permissionTypes.size() == 0) { + return new String[0]; + } + + List converted = new ArrayList<>(); + for (Object permissionType : permissionTypes.toArrayList()) { + if (String.valueOf(permissionType).equals(SampleType.STEP_COUNT) + || String.valueOf(permissionType).equals(SampleType.DISTANCE_WALKING_RUNNING) + || String.valueOf(permissionType).equals(SampleType.WEIGHT) + || String.valueOf(permissionType).equals(SampleType.BLOOD_PRESSURE)) { + converted.add(String.valueOf(permissionType)); + } + } + return converted.toArray(new String[]{}); + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/modules/LoggerBridge.java b/android/src/main/java/nl/sense/rninputkit/modules/LoggerBridge.java new file mode 100644 index 0000000..f31dc3c --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/modules/LoggerBridge.java @@ -0,0 +1,43 @@ +package nl.sense.rninputkit.modules; + + +import android.util.Log; + +import com.erasmus.BuildConfig; +import com.erasmus.helper.LoggerFileWriter; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; + +/** + * Created by panjiyudasetya on 6/21/17. + */ + +public class LoggerBridge extends ReactContextBaseJavaModule { + private static final String LOGGER_MODULE_NAME = "Logger"; + private static final String TAG = LOGGER_MODULE_NAME; + private LoggerFileWriter mLogger; + + @SuppressWarnings("unused") // Used by React Native + public LoggerBridge(ReactApplicationContext reactContext) { + super(reactContext); + if (BuildConfig.IS_DEBUG_MODE_ENABLED) { + mLogger = new LoggerFileWriter(reactContext); + } + } + + @Override + public String getName() { + return LOGGER_MODULE_NAME; + } + + @ReactMethod + @SuppressWarnings("unused")//Used by React Native application + public void log(String message) { + if (BuildConfig.IS_DEBUG_MODE_ENABLED && mLogger != null) { + Log.d(TAG, "[SenseLogger] : " + message); + mLogger.logEvent(System.currentTimeMillis(), TAG, message); + } + } + +} diff --git a/android/src/main/java/nl/sense/rninputkit/modules/health/HealthPermissionPromise.java b/android/src/main/java/nl/sense/rninputkit/modules/health/HealthPermissionPromise.java new file mode 100644 index 0000000..e5b2842 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/modules/health/HealthPermissionPromise.java @@ -0,0 +1,30 @@ +package nl.sense.rninputkit.modules.health; + +import androidx.annotation.NonNull; + +import com.facebook.react.bridge.Promise; + +import nl.sense_os.input_kit.status.IKProviderInfo; + +/** + * Created by panjiyudasetya on 11/20/17. + */ + +public class HealthPermissionPromise { + private Promise promise; + private IKProviderInfo providerInfo; + + public HealthPermissionPromise(@NonNull Promise promise, + @NonNull IKProviderInfo providerInfo) { + this.promise = promise; + this.providerInfo = providerInfo; + } + + public Promise getPromise() { + return promise; + } + + public IKProviderInfo getProviderInfo() { + return providerInfo; + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/modules/health/event/Event.java b/android/src/main/java/nl/sense/rninputkit/modules/health/event/Event.java new file mode 100644 index 0000000..86eae8c --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/modules/health/event/Event.java @@ -0,0 +1,136 @@ +package nl.sense.rninputkit.modules.health.event; + +import android.os.Bundle; +import androidx.annotation.NonNull; + +import com.erasmus.helper.ValueConverter; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.WritableMap; +import com.google.gson.Gson; +import com.google.gson.JsonObject; + +import java.util.List; + +import nl.sense_os.input_kit.entity.IKValue; + +/** + * Created by panjiyudasetya on 7/21/17. + */ + +public class Event { + private static final Gson GSON = new Gson(); + private static final String TOPIC = "topic"; + private static final String SAMPLES = "samples"; + private static final String EVENT_ID = "eventId"; + private static final String EVENT_NAME = "name"; + private String eventId; + private String eventName; + private String topic; + private List> samples; + private Callback completion; + + private Event(@NonNull String eventId, + @NonNull String eventName, + @NonNull String topic, + @NonNull List> samples, + @NonNull Callback completion) { + this.eventId = eventId; + this.eventName = eventName; + this.topic = topic; + this.samples = samples; + this.completion = completion; + } + + public String getEventId() { + return eventId; + } + + public String getEventName() { + return eventName; + } + + public String getTopic() { + return topic; + } + + public List> getSamples() { + return samples; + } + + public Callback getCompletion() { + return completion; + } + + /** + * Convert event into Android {@link Bundle} + * @return {@link Bundle} + */ + public String toJson() { + JsonObject object = new JsonObject(); + object.addProperty(EVENT_ID, eventId); + object.addProperty(EVENT_NAME, eventName); + object.addProperty(TOPIC, topic); + object.addProperty(SAMPLES, GSON.toJson(samples)); + return object.toString(); + } + + /** + * Convert Event payload into writable map + * @return {@link WritableMap} + */ + public WritableMap toWritableMap() { + WritableMap mapValue = Arguments.createMap(); + mapValue.putString(EVENT_ID, eventId); + mapValue.putString(EVENT_NAME, eventName); + mapValue.putString(TOPIC, topic); + + if (samples.isEmpty()) mapValue.putArray(SAMPLES, Arguments.createArray()); + else mapValue.putArray(SAMPLES, ValueConverter.toWritableArray(samples)); + return mapValue; + } + + public static class Builder { + private String newEventId; + private String newEventName; + private String newTopic; + private List> newSamples; + private Callback newCompletion; + + public Builder eventId(@NonNull String newEventId) { + this.newEventId = newEventId; + return this; + } + + public Builder eventName(@NonNull String newEventName) { + this.newEventName = newEventName; + return this; + } + + public Builder topic(@NonNull String newTopic) { + this.newTopic = newTopic; + return this; + } + + + public Builder samples(@NonNull List> newSamples) { + this.newSamples = newSamples; + return this; + } + + public Builder completion(@NonNull Callback newCompletion) { + this.newCompletion = newCompletion; + return this; + } + + public Event build() { + return new Event( + newEventId, + newEventName, + newTopic, + newSamples, + newCompletion + ); + } + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/modules/health/event/EventHandler.java b/android/src/main/java/nl/sense/rninputkit/modules/health/event/EventHandler.java new file mode 100644 index 0000000..993aa92 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/modules/health/event/EventHandler.java @@ -0,0 +1,201 @@ +package nl.sense.rninputkit.modules.health.event; + +import android.content.Context; +import androidx.annotation.NonNull; +import android.util.Log; + +import com.erasmus.modules.LoggerBridge; +import com.erasmus.service.EventHandlerTaskService; +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.LifecycleEventListener; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.modules.core.DeviceEventManagerModule; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import nl.sense_os.input_kit.constant.IKStatus; +import nl.sense_os.input_kit.entity.IKValue; +import nl.sense_os.input_kit.entity.SensorDataPoint; + +/** + * Created by panjiyudasetya on 7/24/17. + */ + +// TODO: should this class have process queue? +public class EventHandler extends ReactContextBaseJavaModule implements LifecycleEventListener { + private static final String EVENT_HANDLER_MODULE_NAME = "EventHandlerBridge"; + private Set mAvailableListeners = new HashSet<>(); + private List mPendingEvents = new ArrayList<>(); + private Map mCompletionBlocks = new HashMap<>(); + + private LoggerBridge mLogger; + private ReactContext mReactContext; + private boolean mIsHostDestroyed; + + public EventHandler(ReactApplicationContext reactContext) { + super(reactContext); + mReactContext = reactContext; + mReactContext.addLifecycleEventListener(this); + + mLogger = new LoggerBridge(reactContext); + mIsHostDestroyed = false; + } + + @Override + public String getName() { + return EVENT_HANDLER_MODULE_NAME; + } + + /** + * Called by JS layer when a listener is ready + * This method can be called from multiple threads + */ + @ReactMethod + @SuppressWarnings("unused")//used by React Native + public void onListenerReady(String name, Promise promise) { + mLogger.log(String.format("new listener: %s became available.", name)); + + mAvailableListeners.add(name); + for (Event event : mPendingEvents) { + if (name.equals(event.getEventName())) { + emit(event); + } + } + promise.resolve(null); + } + + /** + * Called by JS layer when processing event is completed. + * This method can be called from multiple threads + */ + @ReactMethod + @SuppressWarnings("unused")//used by React Native + public void onEventDidProcessed(String eventId, Promise promise) { + Callback completionHandler = mCompletionBlocks.get(eventId); + if (completionHandler == null) { + // TODO: Notify Error! This should never happen + return; + } + + mCompletionBlocks.remove(eventId); + promise.resolve(null); + // This callback potentially triggers everything to be stopped and de-allocated. + completionHandler.invoke(); + } + + /** + * Called by Headless JS whenever this handler catalyst destroyed. + * @param eventId Event Id + * @param eventName Event name + * @param topic Event topic name + * @param payload Payload event in json format + */ + @ReactMethod + @SuppressWarnings("unused")//used by React Native + public void emit(String eventId, String eventName, String topic, String payload, Promise promise) { + List> payloadObjects; + try { + Type typeToken = new TypeToken>() { }.getType(); + payloadObjects = new Gson().fromJson(payload, typeToken); + } catch (Exception e) { + promise.reject( + String.valueOf(IKStatus.Code.INVALID_REQUEST), + "Payload should be in collection format of IKValue!" + ); + return; + } + + emit(new Event.Builder() + .eventId(eventId) + .eventName(eventName) + .topic(topic) + .samples(payloadObjects) + .completion(new Callback() { + @Override + public void invoke(Object... args) { + // TODO: Add completion handler if needed. + } + }) + .build() + ); + promise.resolve(null); + } + + // Not exposed to JS + // called by internal classes to emit event from sensor listener + public static void emit(@NonNull Context context, + @NonNull String eventName, + @NonNull SensorDataPoint dataPoint, + @NonNull Callback completionBlock) { + EventHandlerTaskService.sendEvent( + context, + new Event.Builder() + .eventId(ShortCodeGenerator.generateEventID()) + .eventName(eventName) + .topic(dataPoint.getTopic()) + .samples(dataPoint.getPayload()) + .completion(completionBlock) + .build() + ); + } + + // Not exposed to JS + // called by native components such as Health Kit. + // This method can be called from multiple threads + private void emit(@NonNull Event event) { + mLogger.log("Emitting Event : " + event.toJson()); + + // TODO: this check might be not sufficient if there are multiple listeners per type of event. + if (!mAvailableListeners.contains(event.getEventName())) { + mPendingEvents.add( + new Event.Builder() + .eventId(ShortCodeGenerator.generateEventID()) + .eventName(event.getEventName()) + .topic(event.getTopic()) + .samples(event.getSamples()) + .completion(event.getCompletion()) + .build() + ); + return; + } + + mCompletionBlocks.put(event.getEventId(), event.getCompletion()); + + if (!mIsHostDestroyed) { + mReactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit(event.getEventName(), event.toWritableMap()); + } else EventHandlerTaskService.sendEvent(mReactContext, event); + } + + @Override + public void onHostResume() { + // Do nothing here, as long as host didn't destroyed, we still have an access + // into DeviceEventManagerModule.RCTDeviceEventEmitter + mIsHostDestroyed = false; + } + + @Override + public void onHostPause() { + // Do nothing here, as long as host didn't destroyed, we still have an access + // into DeviceEventManagerModule.RCTDeviceEventEmitter + } + + @Override + public void onHostDestroy() { + Log.d(EVENT_HANDLER_MODULE_NAME, "onHostDestroy: Prepare initialize event handler state"); + mIsHostDestroyed = true; + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/modules/health/event/ShortCodeGenerator.java b/android/src/main/java/nl/sense/rninputkit/modules/health/event/ShortCodeGenerator.java new file mode 100644 index 0000000..7682be4 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/modules/health/event/ShortCodeGenerator.java @@ -0,0 +1,30 @@ +package nl.sense.rninputkit.modules.health.event; + +import java.security.SecureRandom; + +/** + * Created by panjiyudasetya on 7/24/17. + */ + +public class ShortCodeGenerator { + private static final String AB = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + private static SecureRandom rnd = new SecureRandom(); + private ShortCodeGenerator() { } + + public static String generateEventID() { + long currentTimeInSeconds = System.currentTimeMillis() / 1000; + return currentTimeInSeconds + ":" + getCode(4); + } + + private static String getCode(int len) { + StringBuilder sb = new StringBuilder(len); + for (int i = 0; i < len; i++) { + sb.append( + AB.charAt( + rnd.nextInt(AB.length()) + ) + ); + } + return sb.toString(); + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/service/EventHandlerTaskService.java b/android/src/main/java/nl/sense/rninputkit/service/EventHandlerTaskService.java new file mode 100644 index 0000000..c4d5c81 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/service/EventHandlerTaskService.java @@ -0,0 +1,113 @@ +package nl.sense.rninputkit.service; + +import android.app.ActivityManager; +import android.app.Notification; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; + +import com.erasmus.R; +import com.erasmus.modules.health.event.Event; +import com.erasmus.service.activity.detector.Constants; +import com.facebook.react.HeadlessJsTaskService; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.jstasks.HeadlessJsTaskConfig; +import com.google.gson.Gson; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static android.os.Build.VERSION.SDK_INT; + +/** + * Created by panjiyudasetya on 7/26/17. + */ + +public class EventHandlerTaskService extends HeadlessJsTaskService { + private static final String TASK_NAME = "EventHandlerTaskService"; + + @Override + public void onCreate() { + super.onCreate(); + // Enable notification channel to make activity recognition works on Android O + if (SDK_INT >= Build.VERSION_CODES.O) { + Notification notification = new ServiceNotificationCompat.Builder(this) + .channelId(Constants.INPUT_KIT_CHANNEL_ID) + .channelName(getString(R.string.name_of_syncing_steps_channel_desc)) + .iconId(R.mipmap.ic_notif) + .content(getString(R.string.title_of_syncing_steps)) + .build(); + startForeground(Constants.STEP_COUNT_SENSOR_CHANNEL_ID, notification); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (SDK_INT >= Build.VERSION_CODES.O) stopForeground(true); + stopSelf(); + } + + public static void sendEvent(@NonNull final Context context, @NonNull Event event) { + // To see detected event, you can uncomment this to show it on Android notification + NotificationHelper.createNotification( + context, + "New " + event.getTopic() + " detected.", + new Gson().toJson(event.getSamples()), + event.getEventId().hashCode() + ); + + // Since we are not sure executed tasks on JS will slowing down the UI or not, + // it's better for us to prevent any actions while app is in foreground + // https://facebook.github.io/react-native/docs/headless-js-android#caveats + if (isAppOnForeground(context)) { + NotificationHelper.createNotification( + context, + "Unable to send data", + "Discarded this event :\n" + new Gson().toJson(event) + "\nto avoid slow UI", + event.getEventId().hashCode()); + return; + } + + Intent intentService = new Intent(context, EventHandlerTaskService.class); + intentService.putExtra("data_event", event.toJson()); + ContextCompat.startForegroundService(context, intentService); + } + + @Override + protected HeadlessJsTaskConfig getTaskConfig(Intent intent) { + Bundle extras = intent == null ? null : intent.getExtras(); + WritableMap data = extras == null ? null : Arguments.fromBundle(extras); + return new HeadlessJsTaskConfig( + TASK_NAME, + data, + TimeUnit.MINUTES.toMillis(5), + true); + } + + private static boolean isAppOnForeground(@NonNull Context context) { + /** + We need to check if app is in foreground otherwise the app will crash. + http://stackoverflow.com/questions/8489993/check-android-application-is-in-foreground-or-not + **/ + ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + if (activityManager == null) return false; + + List appProcesses = activityManager.getRunningAppProcesses(); + if (appProcesses == null) return false; + + final String packageName = context.getPackageName(); + for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) { + if (appProcess.importance + == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND + && appProcess.processName.equals(packageName)) { + return true; + } + } + return false; + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/service/NotificationHelper.java b/android/src/main/java/nl/sense/rninputkit/service/NotificationHelper.java new file mode 100644 index 0000000..95a82cf --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/service/NotificationHelper.java @@ -0,0 +1,38 @@ +package nl.sense.rninputkit.service; + + +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; + +import com.erasmus.BuildConfig; +import com.erasmus.R; +import com.erasmus.helper.LoggerFileWriter; + +public class NotificationHelper { + + @SuppressWarnings("unused")//Will be used when its necessary + public static void createNotification(@NonNull Context context, + @NonNull String title, + @NonNull String content, + int notificationId) { + if (BuildConfig.IS_NOTIFICATION_DEBUG_ENABLED) { + NotificationCompat.Builder notificationBuilder = + new NotificationCompat.Builder(context, "InputKit") + .setSmallIcon(R.mipmap.ic_launcher) + .setContentTitle(title) + .setContentText(content) + .setNumber(0) + .setBadgeIconType(NotificationCompat.BADGE_ICON_NONE); + NotificationManagerCompat.from(context) + .notify(notificationId, notificationBuilder.build()); + } + + if (BuildConfig.IS_DEBUG_MODE_ENABLED) { + new LoggerFileWriter(context).logEvent(System.currentTimeMillis(), + String.format("==== %s ====", title), + content); + } + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/service/ServiceNotificationCompat.java b/android/src/main/java/nl/sense/rninputkit/service/ServiceNotificationCompat.java new file mode 100644 index 0000000..ce3871e --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/service/ServiceNotificationCompat.java @@ -0,0 +1,78 @@ +package nl.sense.rninputkit.service; + + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Context; +import android.os.Build; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.app.NotificationCompat; +import android.text.TextUtils; + +import static android.os.Build.VERSION.SDK_INT; + +public class ServiceNotificationCompat { + private ServiceNotificationCompat() { } + public static class Builder { + private Context context; + private String channelId; + private String channelName; + private int iconId; + private String title; + private String content; + + public Builder(@NonNull Context context) { + this.context = context; + } + + public Builder channelId(@NonNull String channelId) { + this.channelId = channelId; + return this; + } + + public Builder channelName(@NonNull String channelName) { + this.channelName = channelName; + return this; + } + + public Builder iconId(int iconId) { + this.iconId = iconId; + return this; + } + + public Builder title(@Nullable String title) { + this.title = title; + return this; + } + + public Builder content(@Nullable String content) { + this.content = content; + return this; + } + + public Notification build() { + if (channelId == null) throw new IllegalStateException("Channel ID must be provided."); + if (channelName == null) throw new IllegalStateException("Channel name must be provided."); + + if (SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel(channelId, channelName, + NotificationManager.IMPORTANCE_NONE); + channel.enableLights(false); + channel.enableVibration(false); + channel.setShowBadge(false); + + NotificationManager nm = ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)); + if (nm != null) nm.createNotificationChannel(channel); + } + + NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId) + .setNumber(0) + .setSmallIcon(iconId); + if (!TextUtils.isEmpty(title)) builder.setContentTitle(title); + if (!TextUtils.isEmpty(content)) builder.setContentText(content); + return builder.build(); + } + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/service/activity/detector/ActivityHandler.java b/android/src/main/java/nl/sense/rninputkit/service/activity/detector/ActivityHandler.java new file mode 100644 index 0000000..f1f9826 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/service/activity/detector/ActivityHandler.java @@ -0,0 +1,122 @@ +package nl.sense.rninputkit.service.activity.detector; + +import android.content.Context; +import android.content.Intent; +import androidx.annotation.NonNull; +import android.util.Pair; + +import com.erasmus.data.Constants; +import com.erasmus.modules.health.event.EventHandler; +import com.facebook.react.bridge.Callback; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; + +import nl.sense_os.input_kit.InputKit; +import nl.sense_os.input_kit.entity.DateContent; +import nl.sense_os.input_kit.entity.IKValue; +import nl.sense_os.input_kit.entity.SensorDataPoint; +import nl.sense_os.input_kit.status.IKResultInfo; + +import static com.erasmus.data.Constants.JS_SUPPORTED_EVENTS; +import static nl.sense_os.input_kit.constant.SampleType.DISTANCE_WALKING_RUNNING; +import static nl.sense_os.input_kit.constant.SampleType.STEP_COUNT; + +public class ActivityHandler { + public static final String ACTIVITY_TYPE = "activityType"; + public static final String STEP_DISTANCE_ACTIVITY = STEP_COUNT + "," + DISTANCE_WALKING_RUNNING; + private Context mContext; + + public ActivityHandler(Context context) { + this.mContext = context; + } + + public void proceedIntent(Intent intent) { + if (intent == null || intent.getExtras() == null) return; + + + boolean isStepCountActive = InputKit.getInstance(mContext) + .isPermissionsAuthorised(new String[]{STEP_COUNT}); + + boolean isDistanceActive = InputKit.getInstance(mContext) + .isPermissionsAuthorised(new String[]{DISTANCE_WALKING_RUNNING}); + + String type = intent.getExtras().getString(ACTIVITY_TYPE, ""); + if (type.equals(STEP_DISTANCE_ACTIVITY)) { + if (isStepCountActive) { + emitStepCount(); + } + if (isDistanceActive) { + emitDistance(); + } + } + } + + private void emitStepCount() { + final List> payloads = new ArrayList<>(); + final Pair interval = createInterval(); + InputKit.getInstance(mContext).getStepCount( + new InputKit.Result() { + @Override + public void onNewData(Integer data) { + payloads.add(new IKValue<>( + data, + new DateContent(interval.first), + new DateContent(interval.second) + )); + emit(new SensorDataPoint(STEP_COUNT, payloads)); + } + + @Override + public void onError(@NonNull IKResultInfo error) { } + }); + } + + private void emitDistance() { + final List> payloads = new ArrayList<>(); + final Pair interval = createInterval(); + InputKit.getInstance(mContext).getDistance( + interval.first, + interval.second, + 0, + new InputKit.Result() { + @Override + public void onNewData(Float data) { + payloads.add(new IKValue<>( + data, + new DateContent(interval.first), + new DateContent(interval.second) + )); + emit(new SensorDataPoint(DISTANCE_WALKING_RUNNING, payloads)); + } + + @Override + public void onError(@NonNull IKResultInfo error) { } + }); + } + + private void emit(final SensorDataPoint dataPoint) { + // Emit sensor data point to JS + EventHandler.emit(mContext, + JS_SUPPORTED_EVENTS.get(Constants.EVENTS.inputKitUpdates), + dataPoint, + new Callback() { + @Override + public void invoke(Object... args) { + + } + } // TODO : Does completion callback is necessary? + ); + } + + private Pair createInterval() { + final Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(System.currentTimeMillis()); + cal.set(Calendar.HOUR_OF_DAY, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 0); + return Pair.create(cal.getTimeInMillis(), System.currentTimeMillis()); + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/service/activity/detector/ActivityMonitoringService.java b/android/src/main/java/nl/sense/rninputkit/service/activity/detector/ActivityMonitoringService.java new file mode 100644 index 0000000..854e572 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/service/activity/detector/ActivityMonitoringService.java @@ -0,0 +1,225 @@ +package nl.sense.rninputkit.service.activity.detector; + +import android.app.Notification; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.IBinder; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; + +import com.erasmus.R; +import com.erasmus.service.NotificationHelper; +import com.erasmus.service.ServiceNotificationCompat; +import com.erasmus.service.scheduler.SchedulerCompat; +import com.google.android.gms.location.ActivityRecognitionClient; +import com.google.android.gms.tasks.OnFailureListener; +import com.google.android.gms.tasks.OnSuccessListener; + +import java.text.DateFormat; +import java.util.Date; +import java.util.concurrent.TimeUnit; + +import nl.sense_os.input_kit.constant.SampleType.SampleName; + +import static android.os.Build.VERSION.SDK_INT; + +public class ActivityMonitoringService extends Service { + /** The entry point for interacting with activity recognition. */ + private ActivityRecognitionClient mActivityRecognitionClient; + private boolean mIsActivityUpdateRequested = false; + private ActivityHandler mActivityHandler; + + /** Subscribe activity updates */ + public static void subscribe(@NonNull Context context) { + setRequestUpdateState(context, true); + Intent intentService = new Intent(context, ActivityMonitoringService.class); + intentService.setAction(Constants.SUBSCRIBE_ACTIVITY_UPDATES); + ContextCompat.startForegroundService(context, intentService); + } + + /** Unsubscribe activity updates */ + public static void unsubscribe(@NonNull Context context) { + setRequestUpdateState(context, false); + Intent intentService = new Intent(context, ActivityMonitoringService.class); + intentService.setAction(Constants.UNSUBSCRIBE_ACTIVITY_UPDATES); + ContextCompat.startForegroundService(context, intentService); + } + + /** Restore state of activity updates */ + public static void restoreActivityState(@NonNull Context context) { + Intent intentService = new Intent(context, ActivityMonitoringService.class); + intentService.setAction(Constants.RESTORE_ACTIVITY_UPDATES); + ContextCompat.startForegroundService(context, intentService); + } + + /** Proceed detected activity */ + public static void proceedDetectedActivity(@NonNull Context context, + @NonNull @SampleName String activityType) { + Intent intentService = new Intent(context, ActivityMonitoringService.class); + intentService.putExtra(ActivityHandler.ACTIVITY_TYPE, activityType); + intentService.setAction(Constants.NEW_ACTIVITY_DETECTED); + ContextCompat.startForegroundService(context, intentService); + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onCreate() { + super.onCreate(); + SchedulerCompat.getInstance(this).onCreate(); + mActivityRecognitionClient = new ActivityRecognitionClient(this); + mActivityHandler = new ActivityHandler(this); + + // Enable notification channel to make activity recognition works on Android O + if (SDK_INT >= Build.VERSION_CODES.O) { + Notification notification = new ServiceNotificationCompat.Builder(this) + .channelId(Constants.INPUT_KIT_CHANNEL_ID) + .channelName(getString(R.string.name_of_syncing_steps_channel_desc)) + .iconId(R.mipmap.ic_notif) + .content(getString(R.string.title_of_syncing_steps)) + .build(); + startForeground(Constants.STEP_COUNT_SENSOR_CHANNEL_ID, notification); + } + } + + @Override + public void onTaskRemoved(Intent rootIntent) { + if (didAskRequestUpdate(this)) { + SchedulerCompat.getInstance(this).onDestroy(); + SchedulerCompat.getInstance(this).scheduleImmediately(); + } else { + SchedulerCompat.getInstance(this).cancelSchedules(); + stopService(); + } + super.onTaskRemoved(rootIntent); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + String action = intent.getAction() == null ? "" : intent.getAction(); + if (action.equals(Constants.RESTORE_ACTIVITY_UPDATES)) { + requestActivityUpdates(); + } else if ((action.equals(Constants.SUBSCRIBE_ACTIVITY_UPDATES) && !mIsActivityUpdateRequested)) { + requestActivityUpdates(); + } else if (action.equals(Constants.UNSUBSCRIBE_ACTIVITY_UPDATES)) { + removeActivityUpdates(); + } else if (action.equals(Constants.NEW_ACTIVITY_DETECTED) && didAskRequestUpdate(this)) { + mActivityHandler.proceedIntent(intent); + } else { + stopService(); + } + return START_NOT_STICKY; + } + + /** + * Registers for activity recognition updates using + * {@link ActivityRecognitionClient#requestActivityUpdates(long, PendingIntent)}. + * Registers success and failure callbacks. + */ + public void requestActivityUpdates() { + if (didAskRequestUpdate(this)) { + mActivityRecognitionClient + .requestActivityUpdates(TimeUnit.HOURS.toMillis(2), + getActivityDetectionPendingIntent()) + .addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(Void result) { + NotificationHelper.createNotification( + ActivityMonitoringService.this, + Constants.ACTIVITY_REPORT_TITLE, + "Successfully request for an activity update. It happens at " + + DateFormat.getInstance().format(new Date()), + 5); + } + }) + .addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(@NonNull Exception e) { + NotificationHelper.createNotification( + ActivityMonitoringService.this, + Constants.ACTIVITY_REPORT_TITLE, + "Failure to request for an activity update. It happens at " + + DateFormat.getInstance().format(new Date()), + 6); + } + }); + } + } + + /** + * Removes activity recognition updates using + * {@link ActivityRecognitionClient#removeActivityUpdates(PendingIntent)}. Registers success and + * failure callbacks. + */ + public void removeActivityUpdates() { + mActivityRecognitionClient + .removeActivityUpdates(getActivityDetectionPendingIntent()) + .addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(Void result) { + NotificationHelper.createNotification( + ActivityMonitoringService.this, + Constants.ACTIVITY_REPORT_TITLE, + "Successfully stopping an activity update. It happens at " + + DateFormat.getInstance().format(new Date()), + 7); + stopService(); + } + }) + .addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(@NonNull Exception e) { + NotificationHelper.createNotification( + ActivityMonitoringService.this, + Constants.ACTIVITY_REPORT_TITLE, + "Failure while stopping an activity update. It happens at " + + DateFormat.getInstance().format(new Date()), + 8); + stopService(); + } + }); + } + + /** + * Gets a PendingIntent to be sent for each activity detection. + */ + private PendingIntent getActivityDetectionPendingIntent() { + Intent intent = new Intent(this, DetectedActivitiesIntentService.class); + + // We use FLAG_UPDATE_CURRENT so that we get the same pending intent back when calling + // requestActivityUpdates() and removeActivityUpdates(). + return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + } + + /** + * Retrieves the boolean from SharedPreferences that tracks whether we are requesting activity + * updates. + * @param context current application context + * @return True when did ask request updates. False otherwise. + */ + private static boolean didAskRequestUpdate(@NonNull Context context) { + return ActivityState.getInstance(context).didAskRequestUpdate(); + } + + /** + * Sets the boolean in SharedPreferences that tracks whether activity updates request. + * @param context current application context + * @param requestUpdate True if it's successfully request update, False otherwise. + */ + private static void setRequestUpdateState(@NonNull Context context, boolean requestUpdate) { + ActivityState.getInstance(context).setRequestUpdateState(requestUpdate); + } + + private void stopService() { + if (SDK_INT >= Build.VERSION_CODES.O) stopForeground(true); + stopSelf(); + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/service/activity/detector/ActivityState.java b/android/src/main/java/nl/sense/rninputkit/service/activity/detector/ActivityState.java new file mode 100644 index 0000000..e9ef749 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/service/activity/detector/ActivityState.java @@ -0,0 +1,45 @@ +package nl.sense.rninputkit.service.activity.detector; + +import android.content.Context; +import android.preference.PreferenceManager; +import androidx.annotation.NonNull; + +import java.lang.ref.WeakReference; + +public class ActivityState { + private WeakReference ctxReference; + private static ActivityState sInstance; + + private ActivityState(Context context) { + ctxReference = new WeakReference<>(context); + } + + public static ActivityState getInstance(@NonNull Context context) { + if (sInstance == null || sInstance.ctxReference == null + || sInstance.ctxReference.get() == null) { + sInstance = new ActivityState(context); + } + return sInstance; + } + + /** + * Retrieves the boolean from SharedPreferences that tracks whether we are requesting activity + * updates. + * @return True when did ask request updates. False otherwise. + */ + public boolean didAskRequestUpdate() { + return PreferenceManager.getDefaultSharedPreferences(ctxReference.get()) + .getBoolean(Constants.KEY_ACTIVITY_UPDATES_REQUESTED, false); + } + + /** + * Sets the boolean in SharedPreferences that tracks whether activity updates request. + * @param requestUpdate True if it's successfully request update, False otherwise. + */ + public void setRequestUpdateState(boolean requestUpdate) { + PreferenceManager.getDefaultSharedPreferences(ctxReference.get()) + .edit() + .putBoolean(Constants.KEY_ACTIVITY_UPDATES_REQUESTED, requestUpdate) + .apply(); + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/service/activity/detector/Constants.java b/android/src/main/java/nl/sense/rninputkit/service/activity/detector/Constants.java new file mode 100644 index 0000000..82ee245 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/service/activity/detector/Constants.java @@ -0,0 +1,13 @@ +package nl.sense.rninputkit.service.activity.detector; + +public class Constants { + public static final String INPUT_KIT_CHANNEL_ID = "input_kit_channel"; + public static final int STEP_COUNT_SENSOR_CHANNEL_ID = 1; + public static final String ACTIVITY_REPORT_TITLE = "Activity Update Report"; + public static final String KEY_ACTIVITY_UPDATES_REQUESTED = "KEY_ACTIVITY_UPDATES_REQUESTED"; + public static final String RESTORE_ACTIVITY_UPDATES = "RESTORE_ACTIVITY_UPDATES"; + public static final String SUBSCRIBE_ACTIVITY_UPDATES = "SUBSCRIBE_ACTIVITY_UPDATES"; + public static final String UNSUBSCRIBE_ACTIVITY_UPDATES = "UNSUBSCRIBE_ACTIVITY_UPDATES"; + public static final String NEW_ACTIVITY_DETECTED = "NEW_ACTIVITY_DETECTED"; + private Constants() { } +} diff --git a/android/src/main/java/nl/sense/rninputkit/service/activity/detector/DetectedActivitiesIntentService.java b/android/src/main/java/nl/sense/rninputkit/service/activity/detector/DetectedActivitiesIntentService.java new file mode 100644 index 0000000..07112f1 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/service/activity/detector/DetectedActivitiesIntentService.java @@ -0,0 +1,100 @@ +/** + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nl.sense.rninputkit.service.activity.detector; + +import android.app.IntentService; +import android.content.Intent; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import android.util.SparseIntArray; + +import com.google.android.gms.location.ActivityRecognitionResult; +import com.google.android.gms.location.DetectedActivity; + +import java.util.ArrayList; + +/** + * IntentService for handling incoming intents that are generated as a result of requesting + * activity updates using + * {@link com.google.android.gms.location.ActivityRecognitionClient#requestActivityUpdates(long, + * android.app.PendingIntent)}. + */ +public class DetectedActivitiesIntentService extends IntentService { + + protected static final String TAG = "DetectedActivitiesIS"; + + /** + * This constructor is required, and calls the super IntentService(String) + * constructor with the name for a worker thread. + */ + public DetectedActivitiesIntentService() { + // Use the TAG to name the worker thread. + super(TAG); + } + + @Override + public void onCreate() { + super.onCreate(); + } + + /** + * Handles incoming intents. + * @param intent The Intent is provided (inside a PendingIntent) when requestActivityUpdates() + * is called. + */ + @SuppressWarnings("unchecked") + @Override + protected void onHandleIntent(Intent intent) { + ActivityRecognitionResult result = ActivityRecognitionResult.extractResult(intent); + // Get the list of the probable activities associated with the current state of the + // device. Each activity is associated with a confidence level, which is an int between + // 0 and 100. + ArrayList detectedActivities = (ArrayList) result.getProbableActivities(); + SparseIntArray detectedActivitiesMap = toMap(detectedActivities); + + boolean isWalkingDetected = doesRequirementMatch(detectedActivitiesMap, + DetectedActivity.WALKING, 50); + boolean isRunningDetected = doesRequirementMatch(detectedActivitiesMap, + DetectedActivity.RUNNING, 50); + + if (isWalkingDetected || isRunningDetected) { + ActivityMonitoringService.proceedDetectedActivity(this, + ActivityHandler.STEP_DISTANCE_ACTIVITY); + } + } + + private SparseIntArray toMap(@Nullable ArrayList detectedActivities) { + if (detectedActivities == null || detectedActivities.size() == 0) { + return new SparseIntArray(0); + } + + SparseIntArray detectedActivitiesMap = new SparseIntArray(detectedActivities.size()); + for (DetectedActivity activity : detectedActivities) { + detectedActivitiesMap.put(activity.getType(), activity.getConfidence()); + } + + return detectedActivitiesMap; + } + + private boolean doesRequirementMatch(@NonNull SparseIntArray detectedActivities, + @NonNull Integer expectedActivity, + int expectedConfidenceValue) { + int actualConfidenceValue = detectedActivities.get(expectedActivity, -1) == -1 + ? 0 : detectedActivities.get(expectedActivity); + return actualConfidenceValue > expectedConfidenceValue; + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/service/broadcasts/BootReceiver.java b/android/src/main/java/nl/sense/rninputkit/service/broadcasts/BootReceiver.java new file mode 100644 index 0000000..affa816 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/service/broadcasts/BootReceiver.java @@ -0,0 +1,24 @@ +package nl.sense.rninputkit.service.broadcasts; + + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import com.erasmus.service.activity.detector.ActivityState; +import com.erasmus.service.scheduler.SchedulerCompat; + +public class BootReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (ActivityState.getInstance(context).didAskRequestUpdate()) { + String action = intent == null + ? "" : intent.getAction() == null + ? "" : intent.getAction(); + if (action.equals(Intent.ACTION_BOOT_COMPLETED) + || action.equals(Intent.ACTION_MY_PACKAGE_REPLACED)) { + SchedulerCompat.getInstance(context).scheduleDaily(); + } + } + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/service/scheduler/IScheduler.java b/android/src/main/java/nl/sense/rninputkit/service/scheduler/IScheduler.java new file mode 100644 index 0000000..9a6d703 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/service/scheduler/IScheduler.java @@ -0,0 +1,9 @@ +package com.erasmus.service.scheduler; + +public interface IScheduler { + void onCreate(); + void scheduleDaily(); + void scheduleImmediately(); + void cancelSchedules(); + void onDestroy(); +} diff --git a/android/src/main/java/nl/sense/rninputkit/service/scheduler/JobSchedulerService.java b/android/src/main/java/nl/sense/rninputkit/service/scheduler/JobSchedulerService.java new file mode 100644 index 0000000..2a70ddf --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/service/scheduler/JobSchedulerService.java @@ -0,0 +1,119 @@ +package com.erasmus.service.scheduler; + +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.app.job.JobService; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import androidx.annotation.NonNull; +import com.erasmus.helper.LoggerFileWriter; +import com.erasmus.service.activity.detector.ActivityMonitoringService; + +import java.util.concurrent.TimeUnit; + +import static android.os.Build.VERSION.SDK_INT; + +public class JobSchedulerService extends JobService { + private static final int IMMEDIATELY_JOB_ID = 1; + private static final int DAILY_JOB_ID = 2; + private static final long HALF_DAY_INTERVAL = 12 * 60 * 60 * 1000L; + + private static void logEvent(@NonNull Context context, + @NonNull String message) { + new LoggerFileWriter(context).logEvent(System.currentTimeMillis(), + JobSchedulerService.class.getName(), + message); + } + + private static JobInfo createJobInfo(@NonNull ComponentName componentName, int type) { + JobInfo.Builder builder; + switch (type) { + case DAILY_JOB_ID: + builder = new JobInfo.Builder(DAILY_JOB_ID, componentName) + .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) + .setRequiresDeviceIdle(false) + .setPeriodic(HALF_DAY_INTERVAL); + break; + case IMMEDIATELY_JOB_ID : + default: + builder = new JobInfo.Builder(IMMEDIATELY_JOB_ID, componentName) + .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) + .setRequiresDeviceIdle(false) + .setMinimumLatency(1) + .setOverrideDeadline(TimeUnit.MINUTES.toMillis(1)); + break; + } + return builder.build(); + } + + private static void scheduleEvent(@NonNull Context context, + @NonNull JobInfo jobInfo) { + JobScheduler scheduler; + if (SDK_INT >= Build.VERSION_CODES.M) { + scheduler = context.getSystemService(JobScheduler.class); + } else { + scheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); + } + + if (scheduler == null) { + logEvent(context, "Job scheduler is not available"); + return; + } + + int resultCode = scheduler.schedule(jobInfo); + if (resultCode == JobScheduler.RESULT_SUCCESS) { + logEvent(context, "Job scheduled!"); + } else { + logEvent(context, "Job is not scheduled!"); + } + } + + private static void cancelAllEvent(@NonNull Context context) { + JobScheduler scheduler; + if (SDK_INT >= Build.VERSION_CODES.M) { + scheduler = context.getSystemService(JobScheduler.class); + } else { + scheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); + } + + if (scheduler == null) { + logEvent(context, "Job scheduler is not available"); + return; + } + + scheduler.cancelAll(); + } + + public static void scheduleDaily(@NonNull Context context) { + ComponentName componentName = new ComponentName(context, JobSchedulerService.class); + scheduleEvent(context, createJobInfo(componentName, DAILY_JOB_ID)); + } + + public static void scheduleImmediately(@NonNull Context context) { + ComponentName componentName = new ComponentName(context, JobSchedulerService.class); + scheduleEvent(context, createJobInfo(componentName, IMMEDIATELY_JOB_ID)); + } + + public static void cancelAllSchedule(@NonNull Context context) { + cancelAllEvent(context); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + return START_NOT_STICKY; + } + + @Override + public boolean onStartJob(JobParameters jobParameters) { + ActivityMonitoringService.restoreActivityState(this); + return true; + } + + @Override + public boolean onStopJob(JobParameters jobParameters) { + return false; + } +} \ No newline at end of file diff --git a/android/src/main/java/nl/sense/rninputkit/service/scheduler/SchedulerCompat.java b/android/src/main/java/nl/sense/rninputkit/service/scheduler/SchedulerCompat.java new file mode 100644 index 0000000..02b66a8 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/service/scheduler/SchedulerCompat.java @@ -0,0 +1,67 @@ +package com.erasmus.service.scheduler; + +import android.content.Context; +import android.os.Build; +import androidx.annotation.NonNull; + +import com.erasmus.service.scheduler.v14.AlarmCompat; + +import java.lang.ref.WeakReference; + +import static android.os.Build.VERSION.SDK_INT; + +public class SchedulerCompat implements IScheduler { + private WeakReference ctxReference; + private static SchedulerCompat sInstance; + + private SchedulerCompat(@NonNull Context context) { + ctxReference = new WeakReference<>(context.getApplicationContext()); + } + + public static SchedulerCompat getInstance(@NonNull Context context) { + if (sInstance == null || sInstance.ctxReference == null + || sInstance.ctxReference.get() == null) { + sInstance = new SchedulerCompat(context); + } + return sInstance; + } + + @Override + public void scheduleDaily() { + if (SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + JobSchedulerService.scheduleDaily(ctxReference.get()); + return; + } + AlarmCompat.getInstance(ctxReference.get()).scheduleDaily(); + } + + @Override + public void scheduleImmediately() { + if (SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + JobSchedulerService.scheduleImmediately(ctxReference.get()); + return; + } + AlarmCompat.getInstance(ctxReference.get()).scheduleImmediately(); + } + + @Override + public void cancelSchedules() { + if (SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + JobSchedulerService.cancelAllSchedule(ctxReference.get()); + return; + } + AlarmCompat.getInstance(ctxReference.get()).cancelSchedules(); + } + + @Override + public void onCreate() { + if (SDK_INT >= Build.VERSION_CODES.LOLLIPOP) return; + AlarmCompat.getInstance(ctxReference.get()).onCreate(); + } + + @Override + public void onDestroy() { + if (SDK_INT >= Build.VERSION_CODES.LOLLIPOP) return; + AlarmCompat.getInstance(ctxReference.get()).onDestroy(); + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/service/scheduler/v14/AlarmCompat.java b/android/src/main/java/nl/sense/rninputkit/service/scheduler/v14/AlarmCompat.java new file mode 100644 index 0000000..18fe4a3 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/service/scheduler/v14/AlarmCompat.java @@ -0,0 +1,185 @@ +package com.erasmus.service.scheduler.v14; + + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Build; +import androidx.annotation.NonNull; + +import com.erasmus.service.scheduler.IScheduler; + +import java.lang.ref.WeakReference; +import java.util.Calendar; +import java.util.concurrent.TimeUnit; + +import static android.app.AlarmManager.RTC_WAKEUP; +import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; +import static android.content.Context.ALARM_SERVICE; +import static android.os.Build.VERSION.SDK_INT; +import static java.util.concurrent.TimeUnit.MINUTES; + +public class AlarmCompat implements IScheduler { + private static final String ACTION_SCHEDULE_ALARM_INTENT = "ACTION_SCHEDULE_ALARM_INTENT"; + private static final int PI_SELF_SCHEDULED_ALARM = 1000; + private static final int PI_REPEATING_ALARM = 2000; + private static final AlarmReceiver ALARM_RECEIVER = new AlarmReceiver(); + + private WeakReference ctxReference; + private AlarmManager mAlarmManager; + private boolean didRegisterAlarmReceiver; + private static AlarmCompat sInstance; + + private AlarmCompat(Context context) { + ctxReference = new WeakReference<>(context.getApplicationContext()); + mAlarmManager = (AlarmManager) context.getSystemService(ALARM_SERVICE); + onCreate(); + } + + public static AlarmCompat getInstance(@NonNull Context context) { + if (sInstance == null || sInstance.ctxReference == null + || sInstance.ctxReference.get() == null) { + sInstance = new AlarmCompat(context); + } + return sInstance; + } + + /** + * Register alarm receiver + */ + @Override + public void onCreate() { + if (!didRegisterAlarmReceiver) { + ctxReference.get().registerReceiver(ALARM_RECEIVER, new IntentFilter(ACTION_SCHEDULE_ALARM_INTENT)); + didRegisterAlarmReceiver = true; + } + } + + /** + * Wake up alarm intent immediately. + */ + @Override + public void scheduleImmediately() { + final long fewMinutesFromNow = System.currentTimeMillis() + MINUTES.toMillis(2); + Intent exactIntent = new Intent(ctxReference.get(), AlarmReceiver.class); + setAlarm(fewMinutesFromNow, PI_SELF_SCHEDULED_ALARM, exactIntent); + } + + /** + * Schedule alarm daily + */ + @Override + public void scheduleDaily() { + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR_OF_DAY, 8); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + + final long firstAlarmTimestamp = calendar.getTimeInMillis(); + calendar.set(Calendar.HOUR_OF_DAY, 20); + final long secondAlarmTimestamp = calendar.getTimeInMillis(); + final long oneDayInMillis = TimeUnit.DAYS.toMillis(1); + + Intent alarmIntent = new Intent(ctxReference.get(), AlarmReceiver.class); + // First alarm must be set at 08:00 + repeatAlarm(firstAlarmTimestamp, oneDayInMillis, PI_REPEATING_ALARM, alarmIntent); + // Second alarm must be set at 20:00 + repeatAlarm(secondAlarmTimestamp, oneDayInMillis, PI_REPEATING_ALARM + 1, alarmIntent); + } + + @Override + public void cancelSchedules() { + cancelAlarm(PI_SELF_SCHEDULED_ALARM); + cancelAlarm(PI_REPEATING_ALARM); + cancelAlarm(PI_REPEATING_ALARM + 1); + } + + /** + * Deregister alarm receiver + */ + @Override + public void onDestroy() { + ctxReference.get().unregisterReceiver(ALARM_RECEIVER); + didRegisterAlarmReceiver = false; + } + + /** + * Helper function to fire an alarm at specific time. + * @param triggerAtMillis Start alarm in milliseconds + * @param alarmId Alarm id + * @param exactHandlerIntent Alarm wake up handler intent + */ + @SuppressWarnings("ObsoleteSdkInt") + private void setAlarm(long triggerAtMillis, + int alarmId, + @NonNull Intent exactHandlerIntent) { + PendingIntent pendingIntent = PendingIntent.getBroadcast( + ctxReference.get(), + alarmId, + exactHandlerIntent, + FLAG_UPDATE_CURRENT); + + if (SDK_INT >= Build.VERSION_CODES.M) { + mAlarmManager.setExactAndAllowWhileIdle( + RTC_WAKEUP, + triggerAtMillis, + pendingIntent + ); + } else if (SDK_INT >= Build.VERSION_CODES.KITKAT) { + mAlarmManager.setExact( + RTC_WAKEUP, + triggerAtMillis, + pendingIntent + ); + } else { + mAlarmManager.set( + RTC_WAKEUP, + triggerAtMillis, + pendingIntent + ); + } + } + + /** + * Helper function to create repeating alarm intent + * @param triggerAtMillis Start alarm in milliseconds + * @param intervalInMillis Interval of repeating alarm in milliseconds + * @param alarmId Alarm id + * @param repeatingHandlerIntent Repeating alarm handler intent + */ + private void repeatAlarm(long triggerAtMillis, + long intervalInMillis, + int alarmId, + @NonNull Intent repeatingHandlerIntent) { + PendingIntent pendingIntent = PendingIntent.getBroadcast( + ctxReference.get(), + alarmId, + repeatingHandlerIntent, + FLAG_UPDATE_CURRENT); + + mAlarmManager.setRepeating( + RTC_WAKEUP, + triggerAtMillis, + intervalInMillis, + pendingIntent + ); + } + + /** + * Helper function to cancel repeating alarm intent + * @param alarmId Alarm id + */ + private void cancelAlarm(int alarmId) { + Intent alarmIntent = new Intent(ctxReference.get(), AlarmReceiver.class); + PendingIntent pendingIntent = PendingIntent.getBroadcast( + ctxReference.get(), + alarmId, + alarmIntent, + FLAG_UPDATE_CURRENT); + + mAlarmManager.cancel(pendingIntent); + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/service/scheduler/v14/AlarmReceiver.java b/android/src/main/java/nl/sense/rninputkit/service/scheduler/v14/AlarmReceiver.java new file mode 100644 index 0000000..9575c69 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/service/scheduler/v14/AlarmReceiver.java @@ -0,0 +1,16 @@ +package com.erasmus.service.scheduler.v14; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import com.erasmus.service.EventHandlerTaskService; +import com.erasmus.service.activity.detector.ActivityMonitoringService; + +public class AlarmReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + ActivityMonitoringService.restoreActivityState(context); + EventHandlerTaskService.acquireWakeLockNow(context); + } +} diff --git a/example/android/.project b/example/android/.project new file mode 100644 index 0000000..3964dd3 --- /dev/null +++ b/example/android/.project @@ -0,0 +1,17 @@ + + + android + Project android created by Buildship. + + + + + org.eclipse.buildship.core.gradleprojectbuilder + + + + + + org.eclipse.buildship.core.gradleprojectnature + + From cd3529fa51d841a08398fbe6613fceb17a47de27 Mon Sep 17 00:00:00 2001 From: xaviraol Date: Thu, 19 Dec 2019 15:33:23 +0100 Subject: [PATCH 03/13] imported androidx --- android/build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/android/build.gradle b/android/build.gradle index 5dd382b..c89a430 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -73,6 +73,9 @@ repositories { dependencies { //noinspection GradleDynamicVersion implementation 'com.facebook.react:react-native:+' // From node_modules + + // added by xavi + implementation "androidx.appcompat:appcompat:${safeExtGet('androidxVersion', '1.0.2')}" } def configureReactNativePom(def pom) { From 760acb6466d2ec6a642d0f8e0c630c1042fb0d59 Mon Sep 17 00:00:00 2001 From: xaviraol Date: Thu, 19 Dec 2019 15:36:18 +0100 Subject: [PATCH 04/13] imported google.code.gson --- android/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/android/build.gradle b/android/build.gradle index c89a430..f136530 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -76,6 +76,7 @@ dependencies { // added by xavi implementation "androidx.appcompat:appcompat:${safeExtGet('androidxVersion', '1.0.2')}" + implementation 'com.google.code.gson:gson:2.8.5' } def configureReactNativePom(def pom) { From eb17a41cd708cfa864534e3bf86fe58df5093ee3 Mon Sep 17 00:00:00 2001 From: xaviraol Date: Thu, 19 Dec 2019 15:44:08 +0100 Subject: [PATCH 05/13] Changed old erasmus imports to new rninputkit ones --- .../nl/sense/rninputkit/RNInputKitPackage.java | 6 +++--- .../sense/rninputkit/modules/HealthBridge.java | 18 +++++++++--------- .../sense/rninputkit/modules/LoggerBridge.java | 2 +- .../rninputkit/modules/health/event/Event.java | 2 +- .../modules/health/event/EventHandler.java | 4 ++-- .../service/EventHandlerTaskService.java | 4 ++-- .../rninputkit/service/NotificationHelper.java | 2 +- .../activity/detector/ActivityHandler.java | 6 +++--- .../detector/ActivityMonitoringService.java | 4 ++-- .../service/broadcasts/BootReceiver.java | 2 +- 10 files changed, 25 insertions(+), 25 deletions(-) diff --git a/android/src/main/java/nl/sense/rninputkit/RNInputKitPackage.java b/android/src/main/java/nl/sense/rninputkit/RNInputKitPackage.java index fe48889..b00788e 100644 --- a/android/src/main/java/nl/sense/rninputkit/RNInputKitPackage.java +++ b/android/src/main/java/nl/sense/rninputkit/RNInputKitPackage.java @@ -1,8 +1,8 @@ package nl.sense.rninputkit; -import com.erasmus.modules.HealthBridge; -import com.erasmus.modules.LoggerBridge; -import com.erasmus.modules.health.event.EventHandler; +import nl.sense.rninputkit.modules.HealthBridge; +import nl.sense.rninputkit.modules.LoggerBridge; +import nl.sense.rninputkit.modules.health.event.EventHandler; import com.facebook.react.ReactPackage; import com.facebook.react.bridge.JavaScriptModule; import com.facebook.react.bridge.NativeModule; diff --git a/android/src/main/java/nl/sense/rninputkit/modules/HealthBridge.java b/android/src/main/java/nl/sense/rninputkit/modules/HealthBridge.java index e423e7b..8ef8186 100644 --- a/android/src/main/java/nl/sense/rninputkit/modules/HealthBridge.java +++ b/android/src/main/java/nl/sense/rninputkit/modules/HealthBridge.java @@ -8,14 +8,14 @@ import android.util.Log; import android.util.Pair; -import com.erasmus.data.Constants; -import com.erasmus.data.ProviderName; -import com.erasmus.helper.BloodPressureConverter; -import com.erasmus.helper.ValueConverter; -import com.erasmus.helper.WeightConverter; -import com.erasmus.modules.health.HealthPermissionPromise; -import com.erasmus.modules.health.event.EventHandler; -import com.erasmus.service.activity.detector.ActivityMonitoringService; +import nl.sense.rninputkit.data.Constants; +import nl.sense.rninputkit.data.ProviderName; +import nl.sense.rninputkit.helper.BloodPressureConverter; +import nl.sense.rninputkit.helper.ValueConverter; +import nl.sense.rninputkit.helper.WeightConverter; +import nl.sense.rninputkit.modules.health.HealthPermissionPromise; +import nl.sense.rninputkit.modules.health.event.EventHandler; +import nl.sense.rninputkit.service.activity.detector.ActivityMonitoringService; import com.facebook.react.bridge.ActivityEventListener; import com.facebook.react.bridge.Callback; import com.facebook.react.bridge.LifecycleEventListener; @@ -46,7 +46,7 @@ import nl.sense_os.input_kit.status.IKProviderInfo; import nl.sense_os.input_kit.status.IKResultInfo; -import static com.erasmus.data.Constants.JS_SUPPORTED_EVENTS; +import static nl.sense.rninputkit.data.Constants.JS_SUPPORTED_EVENTS; import static nl.sense_os.input_kit.constant.IKStatus.Code.IK_NOT_CONNECTED; /** diff --git a/android/src/main/java/nl/sense/rninputkit/modules/LoggerBridge.java b/android/src/main/java/nl/sense/rninputkit/modules/LoggerBridge.java index f31dc3c..b86f1b9 100644 --- a/android/src/main/java/nl/sense/rninputkit/modules/LoggerBridge.java +++ b/android/src/main/java/nl/sense/rninputkit/modules/LoggerBridge.java @@ -4,7 +4,7 @@ import android.util.Log; import com.erasmus.BuildConfig; -import com.erasmus.helper.LoggerFileWriter; +import nl.sense.rninputkit.helper.LoggerFileWriter; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; diff --git a/android/src/main/java/nl/sense/rninputkit/modules/health/event/Event.java b/android/src/main/java/nl/sense/rninputkit/modules/health/event/Event.java index 86eae8c..c478019 100644 --- a/android/src/main/java/nl/sense/rninputkit/modules/health/event/Event.java +++ b/android/src/main/java/nl/sense/rninputkit/modules/health/event/Event.java @@ -3,7 +3,7 @@ import android.os.Bundle; import androidx.annotation.NonNull; -import com.erasmus.helper.ValueConverter; +import nl.sense.rninputkit.helper.ValueConverter; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Callback; import com.facebook.react.bridge.WritableMap; diff --git a/android/src/main/java/nl/sense/rninputkit/modules/health/event/EventHandler.java b/android/src/main/java/nl/sense/rninputkit/modules/health/event/EventHandler.java index 993aa92..97a309d 100644 --- a/android/src/main/java/nl/sense/rninputkit/modules/health/event/EventHandler.java +++ b/android/src/main/java/nl/sense/rninputkit/modules/health/event/EventHandler.java @@ -4,8 +4,8 @@ import androidx.annotation.NonNull; import android.util.Log; -import com.erasmus.modules.LoggerBridge; -import com.erasmus.service.EventHandlerTaskService; +import nl.sense.rninputkit.modules.LoggerBridge; +import nl.sense.rninputkit.service.EventHandlerTaskService; import com.facebook.react.bridge.Callback; import com.facebook.react.bridge.LifecycleEventListener; import com.facebook.react.bridge.Promise; diff --git a/android/src/main/java/nl/sense/rninputkit/service/EventHandlerTaskService.java b/android/src/main/java/nl/sense/rninputkit/service/EventHandlerTaskService.java index c4d5c81..9211830 100644 --- a/android/src/main/java/nl/sense/rninputkit/service/EventHandlerTaskService.java +++ b/android/src/main/java/nl/sense/rninputkit/service/EventHandlerTaskService.java @@ -10,8 +10,8 @@ import androidx.core.content.ContextCompat; import com.erasmus.R; -import com.erasmus.modules.health.event.Event; -import com.erasmus.service.activity.detector.Constants; +import nl.sense.rninputkit.modules.health.event.Event; +import nl.sense.rninputkit.service.activity.detector.Constants; import com.facebook.react.HeadlessJsTaskService; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.WritableMap; diff --git a/android/src/main/java/nl/sense/rninputkit/service/NotificationHelper.java b/android/src/main/java/nl/sense/rninputkit/service/NotificationHelper.java index 95a82cf..b705e02 100644 --- a/android/src/main/java/nl/sense/rninputkit/service/NotificationHelper.java +++ b/android/src/main/java/nl/sense/rninputkit/service/NotificationHelper.java @@ -8,7 +8,7 @@ import com.erasmus.BuildConfig; import com.erasmus.R; -import com.erasmus.helper.LoggerFileWriter; +import nl.sense.rninputkit.helper.LoggerFileWriter; public class NotificationHelper { diff --git a/android/src/main/java/nl/sense/rninputkit/service/activity/detector/ActivityHandler.java b/android/src/main/java/nl/sense/rninputkit/service/activity/detector/ActivityHandler.java index f1f9826..071315a 100644 --- a/android/src/main/java/nl/sense/rninputkit/service/activity/detector/ActivityHandler.java +++ b/android/src/main/java/nl/sense/rninputkit/service/activity/detector/ActivityHandler.java @@ -5,8 +5,8 @@ import androidx.annotation.NonNull; import android.util.Pair; -import com.erasmus.data.Constants; -import com.erasmus.modules.health.event.EventHandler; +import nl.sense.rninputkit.data.Constants; +import nl.sense.rninputkit.modules.health.event.EventHandler; import com.facebook.react.bridge.Callback; import java.util.ArrayList; @@ -19,7 +19,7 @@ import nl.sense_os.input_kit.entity.SensorDataPoint; import nl.sense_os.input_kit.status.IKResultInfo; -import static com.erasmus.data.Constants.JS_SUPPORTED_EVENTS; +import static nl.sense.rninputkit.data.Constants.JS_SUPPORTED_EVENTS; import static nl.sense_os.input_kit.constant.SampleType.DISTANCE_WALKING_RUNNING; import static nl.sense_os.input_kit.constant.SampleType.STEP_COUNT; diff --git a/android/src/main/java/nl/sense/rninputkit/service/activity/detector/ActivityMonitoringService.java b/android/src/main/java/nl/sense/rninputkit/service/activity/detector/ActivityMonitoringService.java index 854e572..68aacef 100644 --- a/android/src/main/java/nl/sense/rninputkit/service/activity/detector/ActivityMonitoringService.java +++ b/android/src/main/java/nl/sense/rninputkit/service/activity/detector/ActivityMonitoringService.java @@ -12,8 +12,8 @@ import androidx.core.content.ContextCompat; import com.erasmus.R; -import com.erasmus.service.NotificationHelper; -import com.erasmus.service.ServiceNotificationCompat; +import nl.sense.rninputkit.service.NotificationHelper; +import nl.sense.rninputkit.service.ServiceNotificationCompat; import com.erasmus.service.scheduler.SchedulerCompat; import com.google.android.gms.location.ActivityRecognitionClient; import com.google.android.gms.tasks.OnFailureListener; diff --git a/android/src/main/java/nl/sense/rninputkit/service/broadcasts/BootReceiver.java b/android/src/main/java/nl/sense/rninputkit/service/broadcasts/BootReceiver.java index affa816..335ca42 100644 --- a/android/src/main/java/nl/sense/rninputkit/service/broadcasts/BootReceiver.java +++ b/android/src/main/java/nl/sense/rninputkit/service/broadcasts/BootReceiver.java @@ -5,7 +5,7 @@ import android.content.Context; import android.content.Intent; -import com.erasmus.service.activity.detector.ActivityState; +import nl.sense.rninputkit.service.activity.detector.ActivityState; import com.erasmus.service.scheduler.SchedulerCompat; public class BootReceiver extends BroadcastReceiver { From 34ac5eb9c1de6205b6ed307d6c2d559868943b60 Mon Sep 17 00:00:00 2001 From: xaviraol Date: Thu, 19 Dec 2019 15:49:19 +0100 Subject: [PATCH 06/13] Added play-services-location --- android/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/android/build.gradle b/android/build.gradle index f136530..b6e0ac5 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -77,6 +77,7 @@ dependencies { // added by xavi implementation "androidx.appcompat:appcompat:${safeExtGet('androidxVersion', '1.0.2')}" implementation 'com.google.code.gson:gson:2.8.5' + implementation "com.google.android.gms:play-services-location:${safeExtGet('playServiceVersion', '16.0.0')}" } def configureReactNativePom(def pom) { From 32ae5ab44a2b1c035563221dfe9c49c95f991689 Mon Sep 17 00:00:00 2001 From: xaviraol Date: Thu, 19 Dec 2019 15:49:39 +0100 Subject: [PATCH 07/13] Added TODOs to all missing imports --- .../nl/sense/rninputkit/helper/BloodPressureConverter.java | 2 +- .../main/java/nl/sense/rninputkit/helper/DataConverter.java | 2 +- .../java/nl/sense/rninputkit/helper/LoggerFileWriter.java | 4 ++-- .../java/nl/sense/rninputkit/helper/ValueConverter.java | 4 ++-- .../java/nl/sense/rninputkit/helper/WeightConverter.java | 2 +- .../main/java/nl/sense/rninputkit/modules/HealthBridge.java | 2 +- .../main/java/nl/sense/rninputkit/modules/LoggerBridge.java | 2 +- .../sense/rninputkit/service/EventHandlerTaskService.java | 6 +++--- .../nl/sense/rninputkit/service/NotificationHelper.java | 5 +++-- .../service/activity/detector/ActivityHandler.java | 4 ++-- .../activity/detector/ActivityMonitoringService.java | 4 ++-- 11 files changed, 19 insertions(+), 18 deletions(-) diff --git a/android/src/main/java/nl/sense/rninputkit/helper/BloodPressureConverter.java b/android/src/main/java/nl/sense/rninputkit/helper/BloodPressureConverter.java index 77aa02c..cb25870 100644 --- a/android/src/main/java/nl/sense/rninputkit/helper/BloodPressureConverter.java +++ b/android/src/main/java/nl/sense/rninputkit/helper/BloodPressureConverter.java @@ -8,7 +8,7 @@ import java.util.List; -import nl.sense_os.input_kit.entity.BloodPressure; +import nl.sense_os.input_kit.entity.BloodPressure; // TODO IMPORTS /** * Created by xedi on 10/16/17. diff --git a/android/src/main/java/nl/sense/rninputkit/helper/DataConverter.java b/android/src/main/java/nl/sense/rninputkit/helper/DataConverter.java index 98b794f..a5a9476 100644 --- a/android/src/main/java/nl/sense/rninputkit/helper/DataConverter.java +++ b/android/src/main/java/nl/sense/rninputkit/helper/DataConverter.java @@ -5,7 +5,7 @@ import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.WritableMap; -import nl.sense_os.input_kit.entity.DateContent; +import nl.sense_os.input_kit.entity.DateContent; // TODO IMPORTS /** * Created by xedi on 10/13/17. diff --git a/android/src/main/java/nl/sense/rninputkit/helper/LoggerFileWriter.java b/android/src/main/java/nl/sense/rninputkit/helper/LoggerFileWriter.java index c855c99..3fc600b 100644 --- a/android/src/main/java/nl/sense/rninputkit/helper/LoggerFileWriter.java +++ b/android/src/main/java/nl/sense/rninputkit/helper/LoggerFileWriter.java @@ -4,8 +4,8 @@ import android.os.Environment; import android.util.Log; -import com.erasmus.BuildConfig; -import com.erasmus.R; +import com.erasmus.BuildConfig; // TODO IMPORTS +import com.erasmus.R; // TODO IMPORTS import java.io.File; import java.io.FileOutputStream; diff --git a/android/src/main/java/nl/sense/rninputkit/helper/ValueConverter.java b/android/src/main/java/nl/sense/rninputkit/helper/ValueConverter.java index 6560134..826def8 100644 --- a/android/src/main/java/nl/sense/rninputkit/helper/ValueConverter.java +++ b/android/src/main/java/nl/sense/rninputkit/helper/ValueConverter.java @@ -11,8 +11,8 @@ import java.util.List; -import nl.sense_os.input_kit.entity.DateContent; -import nl.sense_os.input_kit.entity.IKValue; +import nl.sense_os.input_kit.entity.DateContent; // TODO IMPORTS +import nl.sense_os.input_kit.entity.IKValue; // TODO IMPORTS /** * Created by panjiyudasetya on 10/23/17. diff --git a/android/src/main/java/nl/sense/rninputkit/helper/WeightConverter.java b/android/src/main/java/nl/sense/rninputkit/helper/WeightConverter.java index 5ccf3b7..9b3d17c 100644 --- a/android/src/main/java/nl/sense/rninputkit/helper/WeightConverter.java +++ b/android/src/main/java/nl/sense/rninputkit/helper/WeightConverter.java @@ -8,7 +8,7 @@ import java.util.List; -import nl.sense_os.input_kit.entity.Weight; +import nl.sense_os.input_kit.entity.Weight; // TODO IMPORTS /** * Created by xedi on 10/13/17. diff --git a/android/src/main/java/nl/sense/rninputkit/modules/HealthBridge.java b/android/src/main/java/nl/sense/rninputkit/modules/HealthBridge.java index 8ef8186..8f55519 100644 --- a/android/src/main/java/nl/sense/rninputkit/modules/HealthBridge.java +++ b/android/src/main/java/nl/sense/rninputkit/modules/HealthBridge.java @@ -31,7 +31,7 @@ import java.util.List; import java.util.concurrent.TimeUnit; -import nl.sense_os.input_kit.HealthProvider; +import nl.sense_os.input_kit.HealthProvider; // TODO IMPORTS import nl.sense_os.input_kit.HealthProvider.ProviderType; import nl.sense_os.input_kit.InputKit; import nl.sense_os.input_kit.constant.IKStatus; diff --git a/android/src/main/java/nl/sense/rninputkit/modules/LoggerBridge.java b/android/src/main/java/nl/sense/rninputkit/modules/LoggerBridge.java index b86f1b9..798a514 100644 --- a/android/src/main/java/nl/sense/rninputkit/modules/LoggerBridge.java +++ b/android/src/main/java/nl/sense/rninputkit/modules/LoggerBridge.java @@ -3,7 +3,7 @@ import android.util.Log; -import com.erasmus.BuildConfig; +import com.erasmus.BuildConfig; // TODO IMPORTS import nl.sense.rninputkit.helper.LoggerFileWriter; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; diff --git a/android/src/main/java/nl/sense/rninputkit/service/EventHandlerTaskService.java b/android/src/main/java/nl/sense/rninputkit/service/EventHandlerTaskService.java index 9211830..57f5af7 100644 --- a/android/src/main/java/nl/sense/rninputkit/service/EventHandlerTaskService.java +++ b/android/src/main/java/nl/sense/rninputkit/service/EventHandlerTaskService.java @@ -9,13 +9,13 @@ import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; -import com.erasmus.R; +import com.erasmus.R; // TODO IMPORTS import nl.sense.rninputkit.modules.health.event.Event; import nl.sense.rninputkit.service.activity.detector.Constants; -import com.facebook.react.HeadlessJsTaskService; +import com.facebook.react.HeadlessJsTaskService; // TODO IMPORTS import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.WritableMap; -import com.facebook.react.jstasks.HeadlessJsTaskConfig; +import com.facebook.react.jstasks.HeadlessJsTaskConfig; // TODO IMPORTS import com.google.gson.Gson; import java.util.List; diff --git a/android/src/main/java/nl/sense/rninputkit/service/NotificationHelper.java b/android/src/main/java/nl/sense/rninputkit/service/NotificationHelper.java index b705e02..235ce2f 100644 --- a/android/src/main/java/nl/sense/rninputkit/service/NotificationHelper.java +++ b/android/src/main/java/nl/sense/rninputkit/service/NotificationHelper.java @@ -6,8 +6,9 @@ import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; -import com.erasmus.BuildConfig; -import com.erasmus.R; +import com.erasmus.BuildConfig; // TODO IMPORTS +import com.erasmus.R; // TODO IMPORTS + import nl.sense.rninputkit.helper.LoggerFileWriter; public class NotificationHelper { diff --git a/android/src/main/java/nl/sense/rninputkit/service/activity/detector/ActivityHandler.java b/android/src/main/java/nl/sense/rninputkit/service/activity/detector/ActivityHandler.java index 071315a..92cb924 100644 --- a/android/src/main/java/nl/sense/rninputkit/service/activity/detector/ActivityHandler.java +++ b/android/src/main/java/nl/sense/rninputkit/service/activity/detector/ActivityHandler.java @@ -13,7 +13,7 @@ import java.util.Calendar; import java.util.List; -import nl.sense_os.input_kit.InputKit; +import nl.sense_os.input_kit.InputKit; // TODO IMPORTS import nl.sense_os.input_kit.entity.DateContent; import nl.sense_os.input_kit.entity.IKValue; import nl.sense_os.input_kit.entity.SensorDataPoint; @@ -21,7 +21,7 @@ import static nl.sense.rninputkit.data.Constants.JS_SUPPORTED_EVENTS; import static nl.sense_os.input_kit.constant.SampleType.DISTANCE_WALKING_RUNNING; -import static nl.sense_os.input_kit.constant.SampleType.STEP_COUNT; +import static nl.sense_os.input_kit.constant.SampleType.STEP_COUNT; // TODO IMPORTS public class ActivityHandler { public static final String ACTIVITY_TYPE = "activityType"; diff --git a/android/src/main/java/nl/sense/rninputkit/service/activity/detector/ActivityMonitoringService.java b/android/src/main/java/nl/sense/rninputkit/service/activity/detector/ActivityMonitoringService.java index 68aacef..4bfe9a3 100644 --- a/android/src/main/java/nl/sense/rninputkit/service/activity/detector/ActivityMonitoringService.java +++ b/android/src/main/java/nl/sense/rninputkit/service/activity/detector/ActivityMonitoringService.java @@ -11,7 +11,7 @@ import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; -import com.erasmus.R; +import com.erasmus.R; // TODO IMPORTS import nl.sense.rninputkit.service.NotificationHelper; import nl.sense.rninputkit.service.ServiceNotificationCompat; import com.erasmus.service.scheduler.SchedulerCompat; @@ -23,7 +23,7 @@ import java.util.Date; import java.util.concurrent.TimeUnit; -import nl.sense_os.input_kit.constant.SampleType.SampleName; +import nl.sense_os.input_kit.constant.SampleType.SampleName; // TODO IMPORTS import static android.os.Build.VERSION.SDK_INT; From 1a4be6f594f4c6c236fbd95a240f277576120400 Mon Sep 17 00:00:00 2001 From: xaviraol Date: Thu, 19 Dec 2019 16:44:35 +0100 Subject: [PATCH 08/13] more imports, some names, and some buildconfig stuff --- android/build.gradle | 5 +++- android/src/main/AndroidManifest.xml | 2 +- .../java/com/reactlibrary/InputKitModule.java | 27 ------------------- .../com/reactlibrary/InputKitPackage.java | 23 ---------------- .../rninputkit/helper/LoggerFileWriter.java | 4 +-- .../rninputkit/modules/LoggerBridge.java | 2 +- .../service/EventHandlerTaskService.java | 2 +- .../service/NotificationHelper.java | 4 +-- .../detector/ActivityMonitoringService.java | 4 +-- .../service/broadcasts/BootReceiver.java | 2 +- .../service/scheduler/IScheduler.java | 2 +- .../scheduler/JobSchedulerService.java | 6 ++--- .../service/scheduler/SchedulerCompat.java | 4 +-- .../service/scheduler/v14/AlarmCompat.java | 4 +-- .../service/scheduler/v14/AlarmReceiver.java | 6 ++--- 15 files changed, 25 insertions(+), 72 deletions(-) delete mode 100644 android/src/main/java/com/reactlibrary/InputKitModule.java delete mode 100644 android/src/main/java/com/reactlibrary/InputKitPackage.java diff --git a/android/build.gradle b/android/build.gradle index b6e0ac5..0476dde 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -49,6 +49,9 @@ android { targetSdkVersion safeExtGet('targetSdkVersion', DEFAULT_TARGET_SDK_VERSION) versionCode 1 versionName "1.0" + //set to true to allow debugging mode, false otherwise + buildConfigField 'boolean', 'IS_NOTIFICATION_DEBUG_ENABLED', "false" + buildConfigField 'boolean', 'IS_DEBUG_MODE_ENABLED', "false" } lintOptions { abortOnError false @@ -87,7 +90,7 @@ def configureReactNativePom(def pom) { name packageJson.title artifactId packageJson.name version = packageJson.version - group = "com.reactlibrary" + group = "nl.sense.rninputkit" description packageJson.description url packageJson.repository.baseUrl diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 20c90c4..c4816d1 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,4 +1,4 @@ + package="nl.sense.rninputkit"> diff --git a/android/src/main/java/com/reactlibrary/InputKitModule.java b/android/src/main/java/com/reactlibrary/InputKitModule.java deleted file mode 100644 index d9a89c1..0000000 --- a/android/src/main/java/com/reactlibrary/InputKitModule.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.reactlibrary; - -import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.ReactContextBaseJavaModule; -import com.facebook.react.bridge.ReactMethod; -import com.facebook.react.bridge.Callback; - -public class InputKitModule extends ReactContextBaseJavaModule { - - private final ReactApplicationContext reactContext; - - public InputKitModule(ReactApplicationContext reactContext) { - super(reactContext); - this.reactContext = reactContext; - } - - @Override - public String getName() { - return "InputKit"; - } - - @ReactMethod - public void sampleMethod(String stringArgument, int numberArgument, Callback callback) { - // TODO: Implement some actually useful functionality - callback.invoke("Received numberArgument: " + numberArgument + " stringArgument: " + stringArgument); - } -} diff --git a/android/src/main/java/com/reactlibrary/InputKitPackage.java b/android/src/main/java/com/reactlibrary/InputKitPackage.java deleted file mode 100644 index b4e7bcb..0000000 --- a/android/src/main/java/com/reactlibrary/InputKitPackage.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.reactlibrary; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import com.facebook.react.ReactPackage; -import com.facebook.react.bridge.NativeModule; -import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.uimanager.ViewManager; -import com.facebook.react.bridge.JavaScriptModule; - -public class InputKitPackage implements ReactPackage { - @Override - public List createNativeModules(ReactApplicationContext reactContext) { - return Arrays.asList(new InputKitModule(reactContext)); - } - - @Override - public List createViewManagers(ReactApplicationContext reactContext) { - return Collections.emptyList(); - } -} diff --git a/android/src/main/java/nl/sense/rninputkit/helper/LoggerFileWriter.java b/android/src/main/java/nl/sense/rninputkit/helper/LoggerFileWriter.java index 3fc600b..42ae105 100644 --- a/android/src/main/java/nl/sense/rninputkit/helper/LoggerFileWriter.java +++ b/android/src/main/java/nl/sense/rninputkit/helper/LoggerFileWriter.java @@ -4,8 +4,8 @@ import android.os.Environment; import android.util.Log; -import com.erasmus.BuildConfig; // TODO IMPORTS -import com.erasmus.R; // TODO IMPORTS +import nl.sense.rninputkit.BuildConfig; +import nl.sense.rninputkit.R; import java.io.File; import java.io.FileOutputStream; diff --git a/android/src/main/java/nl/sense/rninputkit/modules/LoggerBridge.java b/android/src/main/java/nl/sense/rninputkit/modules/LoggerBridge.java index 798a514..0a833e4 100644 --- a/android/src/main/java/nl/sense/rninputkit/modules/LoggerBridge.java +++ b/android/src/main/java/nl/sense/rninputkit/modules/LoggerBridge.java @@ -3,7 +3,7 @@ import android.util.Log; -import com.erasmus.BuildConfig; // TODO IMPORTS +import nl.sense.rninputkit.BuildConfig; // TODO IMPORTS import nl.sense.rninputkit.helper.LoggerFileWriter; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; diff --git a/android/src/main/java/nl/sense/rninputkit/service/EventHandlerTaskService.java b/android/src/main/java/nl/sense/rninputkit/service/EventHandlerTaskService.java index 57f5af7..867c90d 100644 --- a/android/src/main/java/nl/sense/rninputkit/service/EventHandlerTaskService.java +++ b/android/src/main/java/nl/sense/rninputkit/service/EventHandlerTaskService.java @@ -9,7 +9,7 @@ import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; -import com.erasmus.R; // TODO IMPORTS +import nl.sense.rninputkit.R; // TODO IMPORTS import nl.sense.rninputkit.modules.health.event.Event; import nl.sense.rninputkit.service.activity.detector.Constants; import com.facebook.react.HeadlessJsTaskService; // TODO IMPORTS diff --git a/android/src/main/java/nl/sense/rninputkit/service/NotificationHelper.java b/android/src/main/java/nl/sense/rninputkit/service/NotificationHelper.java index 235ce2f..534b555 100644 --- a/android/src/main/java/nl/sense/rninputkit/service/NotificationHelper.java +++ b/android/src/main/java/nl/sense/rninputkit/service/NotificationHelper.java @@ -6,8 +6,8 @@ import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; -import com.erasmus.BuildConfig; // TODO IMPORTS -import com.erasmus.R; // TODO IMPORTS +import nl.sense.rninputkit.BuildConfig; +import nl.sense.rninputkit.R; // TODO IMPORTS import nl.sense.rninputkit.helper.LoggerFileWriter; diff --git a/android/src/main/java/nl/sense/rninputkit/service/activity/detector/ActivityMonitoringService.java b/android/src/main/java/nl/sense/rninputkit/service/activity/detector/ActivityMonitoringService.java index 4bfe9a3..f427c99 100644 --- a/android/src/main/java/nl/sense/rninputkit/service/activity/detector/ActivityMonitoringService.java +++ b/android/src/main/java/nl/sense/rninputkit/service/activity/detector/ActivityMonitoringService.java @@ -11,10 +11,10 @@ import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; -import com.erasmus.R; // TODO IMPORTS +import nl.sense.rninputkit.R; // TODO IMPORTS import nl.sense.rninputkit.service.NotificationHelper; import nl.sense.rninputkit.service.ServiceNotificationCompat; -import com.erasmus.service.scheduler.SchedulerCompat; +import nl.sense.rninputkit.service.scheduler.SchedulerCompat; import com.google.android.gms.location.ActivityRecognitionClient; import com.google.android.gms.tasks.OnFailureListener; import com.google.android.gms.tasks.OnSuccessListener; diff --git a/android/src/main/java/nl/sense/rninputkit/service/broadcasts/BootReceiver.java b/android/src/main/java/nl/sense/rninputkit/service/broadcasts/BootReceiver.java index 335ca42..50c0415 100644 --- a/android/src/main/java/nl/sense/rninputkit/service/broadcasts/BootReceiver.java +++ b/android/src/main/java/nl/sense/rninputkit/service/broadcasts/BootReceiver.java @@ -6,7 +6,7 @@ import android.content.Intent; import nl.sense.rninputkit.service.activity.detector.ActivityState; -import com.erasmus.service.scheduler.SchedulerCompat; +import nl.sense.rninputkit.service.scheduler.SchedulerCompat; public class BootReceiver extends BroadcastReceiver { @Override diff --git a/android/src/main/java/nl/sense/rninputkit/service/scheduler/IScheduler.java b/android/src/main/java/nl/sense/rninputkit/service/scheduler/IScheduler.java index 9a6d703..657e119 100644 --- a/android/src/main/java/nl/sense/rninputkit/service/scheduler/IScheduler.java +++ b/android/src/main/java/nl/sense/rninputkit/service/scheduler/IScheduler.java @@ -1,4 +1,4 @@ -package com.erasmus.service.scheduler; +package nl.sense.rninputkit.service.scheduler; public interface IScheduler { void onCreate(); diff --git a/android/src/main/java/nl/sense/rninputkit/service/scheduler/JobSchedulerService.java b/android/src/main/java/nl/sense/rninputkit/service/scheduler/JobSchedulerService.java index 2a70ddf..b469bcb 100644 --- a/android/src/main/java/nl/sense/rninputkit/service/scheduler/JobSchedulerService.java +++ b/android/src/main/java/nl/sense/rninputkit/service/scheduler/JobSchedulerService.java @@ -1,4 +1,4 @@ -package com.erasmus.service.scheduler; +package nl.sense.rninputkit.service.scheduler; import android.app.job.JobInfo; import android.app.job.JobParameters; @@ -9,8 +9,8 @@ import android.content.Intent; import android.os.Build; import androidx.annotation.NonNull; -import com.erasmus.helper.LoggerFileWriter; -import com.erasmus.service.activity.detector.ActivityMonitoringService; +import nl.sense.rninputkit.helper.LoggerFileWriter; +import nl.sense.rninputkit.service.activity.detector.ActivityMonitoringService; import java.util.concurrent.TimeUnit; diff --git a/android/src/main/java/nl/sense/rninputkit/service/scheduler/SchedulerCompat.java b/android/src/main/java/nl/sense/rninputkit/service/scheduler/SchedulerCompat.java index 02b66a8..3ca246f 100644 --- a/android/src/main/java/nl/sense/rninputkit/service/scheduler/SchedulerCompat.java +++ b/android/src/main/java/nl/sense/rninputkit/service/scheduler/SchedulerCompat.java @@ -1,10 +1,10 @@ -package com.erasmus.service.scheduler; +package nl.sense.rninputkit.service.scheduler; import android.content.Context; import android.os.Build; import androidx.annotation.NonNull; -import com.erasmus.service.scheduler.v14.AlarmCompat; +import nl.sense.rninputkit.service.scheduler.v14.AlarmCompat; import java.lang.ref.WeakReference; diff --git a/android/src/main/java/nl/sense/rninputkit/service/scheduler/v14/AlarmCompat.java b/android/src/main/java/nl/sense/rninputkit/service/scheduler/v14/AlarmCompat.java index 18fe4a3..7e738c1 100644 --- a/android/src/main/java/nl/sense/rninputkit/service/scheduler/v14/AlarmCompat.java +++ b/android/src/main/java/nl/sense/rninputkit/service/scheduler/v14/AlarmCompat.java @@ -1,4 +1,4 @@ -package com.erasmus.service.scheduler.v14; +package nl.sense.rninputkit.service.scheduler.v14; import android.app.AlarmManager; @@ -9,7 +9,7 @@ import android.os.Build; import androidx.annotation.NonNull; -import com.erasmus.service.scheduler.IScheduler; +import nl.sense.rninputkit.service.scheduler.IScheduler; import java.lang.ref.WeakReference; import java.util.Calendar; diff --git a/android/src/main/java/nl/sense/rninputkit/service/scheduler/v14/AlarmReceiver.java b/android/src/main/java/nl/sense/rninputkit/service/scheduler/v14/AlarmReceiver.java index 9575c69..8ab90d8 100644 --- a/android/src/main/java/nl/sense/rninputkit/service/scheduler/v14/AlarmReceiver.java +++ b/android/src/main/java/nl/sense/rninputkit/service/scheduler/v14/AlarmReceiver.java @@ -1,10 +1,10 @@ -package com.erasmus.service.scheduler.v14; +package nl.sense.rninputkit.service.scheduler.v14; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import com.erasmus.service.EventHandlerTaskService; -import com.erasmus.service.activity.detector.ActivityMonitoringService; +import nl.sense.rninputkit.service.EventHandlerTaskService; +import nl.sense.rninputkit.service.activity.detector.ActivityMonitoringService; public class AlarmReceiver extends BroadcastReceiver { From 75d9cc8ebb15c415f9cba1b35423a17265b321a6 Mon Sep 17 00:00:00 2001 From: xaviraol Date: Fri, 20 Dec 2019 15:00:02 +0100 Subject: [PATCH 09/13] Added inputkit and rest of things. It builds in AS --- .../org.eclipse.buildship.core.prefs | 2 + android/build.gradle | 7 +- android/libs/samsung-health-data-v1.3.0.jar | Bin 0 -> 264075 bytes android/libs/sdk-v1.0.0.jar | Bin 0 -> 1996 bytes android/src/main/AndroidManifest.xml | 50 +- .../helper/BloodPressureConverter.java | 2 +- .../rninputkit/helper/DataConverter.java | 2 +- .../rninputkit/helper/ValueConverter.java | 4 +- .../rninputkit/helper/WeightConverter.java | 2 +- .../rninputkit/inputkit/HealthProvider.java | 342 ++++++++ .../inputkit/HealthTrackerState.java | 108 +++ .../sense/rninputkit/inputkit/InputKit.java | 349 ++++++++ .../nl/sense/rninputkit/inputkit/Options.java | 151 ++++ .../inputkit/constant/ApiPermissions.java | 17 + .../inputkit/constant/Constant.java | 11 + .../inputkit/constant/DataSampling.java | 13 + .../inputkit/constant/IKStatus.java | 44 ++ .../inputkit/constant/Interval.java | 31 + .../inputkit/constant/RequiredApp.java | 12 + .../inputkit/constant/SampleType.java | 42 + .../inputkit/entity/BloodPressure.java | 94 +++ .../inputkit/entity/DateContent.java | 61 ++ .../rninputkit/inputkit/entity/IKValue.java | 116 +++ .../inputkit/entity/SensorDataPoint.java | 36 + .../rninputkit/inputkit/entity/Step.java | 11 + .../inputkit/entity/StepContent.java | 52 ++ .../inputkit/entity/TimeInterval.java | 87 ++ .../rninputkit/inputkit/entity/Weight.java | 72 ++ .../inputkit/googlefit/FitPermissionSet.java | 67 ++ .../googlefit/GoogleFitHealthProvider.java | 744 ++++++++++++++++++ .../googlefit/history/DataNormalizer.java | 399 ++++++++++ .../history/DistanceHistoryTask.java | 145 ++++ .../googlefit/history/FitHistory.java | 285 +++++++ .../googlefit/history/HistoryExtractor.java | 167 ++++ .../googlefit/history/HistoryTaskFactory.java | 139 ++++ .../googlefit/history/IFitReader.java | 31 + .../googlefit/history/SafeRequestHandler.java | 137 ++++ .../history/StepCountHistoryTask.java | 172 ++++ .../googlefit/sensor/DistanceSensor.java | 35 + .../inputkit/googlefit/sensor/SensorApi.java | 182 +++++ .../googlefit/sensor/SensorManager.java | 304 +++++++ .../googlefit/sensor/SensorOptions.java | 105 +++ .../inputkit/googlefit/sensor/StepSensor.java | 35 + .../inputkit/googlefit/sensor/Validator.java | 25 + .../rninputkit/inputkit/helper/AppHelper.java | 77 ++ .../inputkit/helper/CollectionUtils.java | 55 ++ .../inputkit/helper/InputKitTimeUtils.java | 238 ++++++ .../inputkit/helper/PreferenceHelper.java | 75 ++ .../inputkit/shealth/BloodPressureReader.java | 80 ++ .../inputkit/shealth/SHealthConstant.java | 42 + .../inputkit/shealth/SHealthWrapper.java | 308 ++++++++ .../shealth/SamsungHealthProvider.java | 253 ++++++ .../inputkit/shealth/SleepReader.java | 73 ++ .../inputkit/shealth/StepBinningData.java | 17 + .../inputkit/shealth/StepCountReader.java | 307 ++++++++ .../inputkit/shealth/StepMonitor.java | 131 +++ .../inputkit/shealth/WeightReader.java | 78 ++ .../inputkit/shealth/utils/DataMapper.java | 144 ++++ .../shealth/utils/SHealthPermissionSet.java | 65 ++ .../inputkit/shealth/utils/SHealthUtils.java | 78 ++ .../inputkit/status/IKProviderInfo.java | 23 + .../inputkit/status/IKResultInfo.java | 42 + .../rninputkit/modules/HealthBridge.java | 34 +- .../health/HealthPermissionPromise.java | 2 +- .../modules/health/event/Event.java | 2 +- .../modules/health/event/EventHandler.java | 6 +- .../activity/detector/ActivityHandler.java | 14 +- .../detector/ActivityMonitoringService.java | 2 +- .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3418 bytes android/src/main/res/mipmap-hdpi/ic_notif.png | Bin 0 -> 583 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2206 bytes android/src/main/res/mipmap-mdpi/ic_notif.png | Bin 0 -> 385 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4842 bytes .../src/main/res/mipmap-xhdpi/ic_notif.png | Bin 0 -> 837 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7718 bytes .../src/main/res/mipmap-xxhdpi/ic_notif.png | Bin 0 -> 1080 bytes android/src/main/res/values/strings.xml | 5 + android/src/main/res/values/styles.xml | 8 + .../org.eclipse.buildship.core.prefs | 2 + 79 files changed, 6814 insertions(+), 37 deletions(-) create mode 100644 android/.settings/org.eclipse.buildship.core.prefs create mode 100755 android/libs/samsung-health-data-v1.3.0.jar create mode 100755 android/libs/sdk-v1.0.0.jar create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/HealthProvider.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/HealthTrackerState.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/InputKit.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/Options.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/constant/ApiPermissions.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/constant/Constant.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/constant/DataSampling.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/constant/IKStatus.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/constant/Interval.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/constant/RequiredApp.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/constant/SampleType.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/entity/BloodPressure.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/entity/DateContent.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/entity/IKValue.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/entity/SensorDataPoint.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/entity/Step.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/entity/StepContent.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/entity/TimeInterval.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/entity/Weight.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/FitPermissionSet.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/GoogleFitHealthProvider.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/history/DataNormalizer.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/history/DistanceHistoryTask.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/history/FitHistory.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/history/HistoryExtractor.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/history/HistoryTaskFactory.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/history/IFitReader.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/history/SafeRequestHandler.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/history/StepCountHistoryTask.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/sensor/DistanceSensor.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/sensor/SensorApi.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/sensor/SensorManager.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/sensor/SensorOptions.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/sensor/StepSensor.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/sensor/Validator.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/helper/AppHelper.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/helper/CollectionUtils.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/helper/InputKitTimeUtils.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/helper/PreferenceHelper.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/shealth/BloodPressureReader.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/shealth/SHealthConstant.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/shealth/SHealthWrapper.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/shealth/SamsungHealthProvider.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/shealth/SleepReader.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/shealth/StepBinningData.java create mode 100755 android/src/main/java/nl/sense/rninputkit/inputkit/shealth/StepCountReader.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/shealth/StepMonitor.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/shealth/WeightReader.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/shealth/utils/DataMapper.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/shealth/utils/SHealthPermissionSet.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/shealth/utils/SHealthUtils.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/status/IKProviderInfo.java create mode 100644 android/src/main/java/nl/sense/rninputkit/inputkit/status/IKResultInfo.java create mode 100644 android/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100755 android/src/main/res/mipmap-hdpi/ic_notif.png create mode 100644 android/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100755 android/src/main/res/mipmap-mdpi/ic_notif.png create mode 100644 android/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100755 android/src/main/res/mipmap-xhdpi/ic_notif.png create mode 100644 android/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100755 android/src/main/res/mipmap-xxhdpi/ic_notif.png create mode 100644 android/src/main/res/values/strings.xml create mode 100644 android/src/main/res/values/styles.xml create mode 100644 example/android/.settings/org.eclipse.buildship.core.prefs diff --git a/android/.settings/org.eclipse.buildship.core.prefs b/android/.settings/org.eclipse.buildship.core.prefs new file mode 100644 index 0000000..e889521 --- /dev/null +++ b/android/.settings/org.eclipse.buildship.core.prefs @@ -0,0 +1,2 @@ +connection.project.dir= +eclipse.preferences.version=1 diff --git a/android/build.gradle b/android/build.gradle index 0476dde..bdc4a76 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -20,7 +20,7 @@ def safeExtGet(prop, fallback) { } apply plugin: 'com.android.library' -apply plugin: 'maven' +// apply plugin: 'maven' buildscript { // The Android Gradle plugin is only required when opening the android folder stand-alone. @@ -81,6 +81,11 @@ dependencies { implementation "androidx.appcompat:appcompat:${safeExtGet('androidxVersion', '1.0.2')}" implementation 'com.google.code.gson:gson:2.8.5' implementation "com.google.android.gms:play-services-location:${safeExtGet('playServiceVersion', '16.0.0')}" + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'androidx.appcompat:appcompat:1.0.2' + implementation "com.google.android.gms:play-services-fitness:${safeExtGet('fitnessApiVersion', '16.0.1')}" + implementation "com.google.android.gms:play-services-awareness:${safeExtGet('awarenessApiVersion', '16.0.0')}" + implementation "com.google.android.gms:play-services-auth:${safeExtGet('authApiVersion', '16.0.1')}" } def configureReactNativePom(def pom) { diff --git a/android/libs/samsung-health-data-v1.3.0.jar b/android/libs/samsung-health-data-v1.3.0.jar new file mode 100755 index 0000000000000000000000000000000000000000..9b21934398f2874bb6ef014413ceece9b67388c8 GIT binary patch literal 264075 zcmd?RRajjMwylc=cXxMpcXtTx?o8Y*!69gHhv4q6!Ciy9y9W&LIjhcn zynMhTFZ`qR)>`jlD$0O@p#nidLIME+5xxBU`Nax^1Y~Sy!{B6Sq1Z)j-(P*s5gg81{#00Gr2%PA}iBDO!O zj~4q=x@dXsd_kv1E1m?Eyg$#-j6)AJb zXR=|gd<2m&AF%&=c!kj<&S0$36YbX$YrG+kIyb(l?m*{_16~9Nk`nk8uUYLw#4$6y z2VtM5fE5OM6!7@BD6#NnqH&M0HxNyv1CRuI!f!gFyZuD>&b{1YTMsn_w@Oxw$vZcE zXXC?H52)g$R^?kT)lRNaN^825qNrHVMsc<`tHKr)?whz7`E|r3lf#K*6d0}}0W=I7 z4xlp13bM8phQ(z#Jf$-Igqbpn?XVih$!O|AiaAq;3qYq4zVj>Mc8t-wm6|@If$&MG zEn`4;IuD$QN3s$sD?aQg20Ez$MYSAeD+Lcr_xN^FguUEgL+rZ6o(5( zdvWg}D056FilJcat+WfMZWRY6_@?Mcsz@lkJR3@uRC~J2WF`QzwA?c z<%2V+OpdqcX{z(2TG(G`){5Vg>Y5Uy(L@q%IK>Lps;z?)Zf7c;r@Du`SifbvoM}(l z-s4{=*TXNi8rrjPyJcQJQUBClCZI6))CT}Z(Dz!Ml0Y$M%T!-L(!snyNR&7C4FSJy zR#uC27|Z$OlSwjm)M7GM%2J#rmqT;S+6b#36LhbFo5g<>wMn-J3Y%b54dg7Z?$$Dg z4WwgrFtp{d{$nWf2YiU1Biqw^a8HpFphM#;1VtGLNKO6mLpl&3AeJ{kK#G6KC->ju zQ}QROuaGL4I@tkS|BBK`rI9EUA=tKk8gF65AWgxQ+d6Du?xiqmRR4Pm3X zH!JpQHS}#36q1y>jmM^dt_W$_^utZN+EKO8fhqHtI0{iEoLi~h1VV_%+?-`o*__VP zk#xCf3!!Mv;-j%xT>=gQk^ym5E^bgRFrv7*x3pe#{7T`v>l>*gVaObBd||(+HbmjQ z+A+!RUCEfEL0Dc$^MjCQgvNxon|wy1pzdpx4!fL9x?GioS?!2v=Qo``2NuXOaJ)p4 z$v#Mv37cWyk%imLS#2WOrpuXJ2+pb-mzu?t#)GDSKv^jI38B?R0Z-n-hsIl$Gs*0k zW~eVtE!@X_*bz}K1GN<+8emo2-xPdH@Q0M!qx1$kC;$T&-badgVV-Q*J04gLxKMo( zDDgthZ2K~UW0p|;mMrOabWhK9za0DxyB-3A8-WuOgFT@2_wVXI3&hSDDtzI62>i|r=lTfpwcK#p+ zuwnu%p=&L6fg~*^`vs_N#d7iA z_8$O6X%;&APLSM?*V|XZAG2S~DnTG@Ltgp%e~eA1bdg8ZQE(X`;bPw(Dx7RWU-MmnEX#=MEukZz!$RD-&`mZMa^uxKZYv1e43Xc`jIA$mRr;>& zVy@hK*i}BMX38fXrHO@VB61w|!rO@Jwdv`!ac}O7PSkOuM)@`?7~BlLpl$OB{Z6!p zl@PQxR}<+lKe@yjGw}Bq%bmqeia5-q!|}F*>>zm>Ne*36d2^3?Emiig_TwDsyNuIe zPI?YP5J@Ijn?vs`gVlZZ1i2>Ol^fvk2WGwS*5bg}OYoBM1E3I4WS)|A8m5D>A2+6Qw~NvMvUh;+Z!%qI>=K)RoBa=vg!m(+5tPE%k)33ND;`$ zH^-nOwdV+l{*z%&k}b*v`<(~-r-)N|a0zZWas z`k*mwq%k#DiwJ|GgtHeX6|#q|9=4Un%lQS;F$|Cfs7y^_!5~9Im}h@K zx9ugJS^Ew(2{Vu^gBN7WsFO44s#WaA6aIWfBmt$w9ZzIdb@FYsk26Wf)4Y^sLn25^ zmM+RvI}O1gqWWRnH)m*l{f4iW@24|n$H}n$r-<*lfxmgpLeQ)agbhyxpfeDDpM4-+pF5+BNC@bcXDi2<(B__ZQ+hEHHoK-p-2%K0POk=#Pt_??@2QmRP)R#?iol z4)aZzEMQ`!3Gb#GOia+aE^rx5kkE`@i25PvtqQ;4XXySQ>iT?pL40Ry1Uv-6XGqqG z>N}<~*n>=USaSA0I2|7MBusA6Fn>=e1@(B*gA>m&l7^q)35B4;lu~0hhI=ZqBTjec z4RL0++f6sBt6Jhvu<1punk=&B%>$D$&%RX?ILMfM$RVsV8_1NWc1{m5e4NWo0W$bW zxyu9)#T*sGQo5#SPe#cI>3$7v(0oKSFYH3+oHVzN#FcUx#l&|VCuz^m ztKhOKL&T3|v_BM_j=arWlNlqOPiake{n~;b#|k#=d#5ywD6B~`nC3^bE zi-y8^SLhp)Q9YV?5#(?S)3_@x&bJ=9!W!=;i8IS{n=rw;nA3~UeMf{lKiWoaGLn3a zt@Aupl#GcvS^-@tRa_$P9WD|JYju-VWp*fl$gZh|x^8db|5^=a z6_y`H!Y~DfZm=-NAmpwP>-?>wfLDzFtzj2iBObSTMj-&HQ)63J=ONb8()X*iG5Fbu{Vj2GvOYO9S;n zn#pmR82j|$9_($bd=Q7{Q`asu;-OL=-Q%hFzy^%cF@k!w-e_rMeL;0naTw-P%358F z3+}RW7Qlwx*z}PKKjPqFB%TnBvTwJ=aF_JeTjumt2Oh|rRK7|gcE*?}+%B9}trJ7I z7$kiZ9ON9s-#v`3n;dYZ8aTjD@eBHuwK87w3M_sJdtr+ZwH`mI%+5y8s za|cFm6l6R2dPDamKF{sErEUF=r%BYj0}r+ z?t0P0LB!YZ>POFW$I zc)x^M(bUn#(#grv&Q{LQ*3kT~GT8q%(EeDY=%_>+5Gy*3Ok+NSt_4Ty#uU@tlDw`Q z90U6BZUdbDS}VMy{a^w%w)l;`(oq$ zb1NLCKkoZd6L!2w933`i^kHdSJl@2_6gc0czGKAflv(GN&^x*}+2|43djKa*Dm-v3NqSsjUZ6rcZ@yyRb!m-=h+ zdVEPs%;i~4hPh8%b9fUi^u}*H%`UTm zRNG^dJZ$)1lY8Ms8!2jW4u2%Lhn_efQt`W`KXYis+BYS~Mp~h?hHa1at><%g145!` z+*)p%_bt;KPPIRLO%;;HrPuI7Z^6pK`Lm6b>Vg46n9K9ShV~J9Xd?cx+?JA-0-rV= z^s2zC1-K8#KbPiJ;$gLPQ zfiYnF=H^R`J*2~sF;_hfwco|7fLDMTXTfH6vCU{Sk3{Z+oy(dN;IQSReg-gWP-K~;8Ja>?JdQY3*eiLj^1PjwdLleBNRdw4&JzgvZ+fty9SbX7NI)kjHrJI$n`Qq zaOzjnvZ{=R@Z2~g$Dvq((`|XUqR%G?B{$M>_S4M(n#?$U2>NR%TSub|NDllJ#OCKR zKAd+qC%2fMtK;=oQmHbTz3`G0L(L|5#xj_DZfS_~2Zt%0(3!VFmHLd2EqzR*hxgaT z-4~ARwD4l$Z+!#?Ma;vZ`v_j;&zMQ{Q1zv{H27_G$@4c!{xj7m7&)0b{$+O=q&#Lj zEr{6hNVONZMCk(LaQr5f&`)$E+sCik4H@C`$-+Eiu;k%vbU{uu52UgF$Jkxt_N>v{ zE|58<$P_nSztS5vZ|~<#6yBU9qLU441)Cuzdm8A{q>J1p?2cJ`yvdXJi=O`D2rieWc`DCZjoN0|tg^^GyB(wHclEc0DlXNCzJ_Z*EoTm9aPlG$h zOOY&>!HzMwSb1ADoHCvqtZ0S?Y1d$@sU}pdZjWnt(>amUGw%;ppf^_Xbayzz!>p;z zT2=e*Ph^}egu@L8O0;{a{b=h;+<0t+YJYn@VfJ9k%&QIrVH$ur7Z+VR5Uc6MFdzEI7+65CQ7CbE~ZY-Qa1L0 zze>>mUT*qhp-aMs`w`eXt@g9t`JKwnejbo7Kud5xv2~1|IDRl|*XzPQv*Lj|hX_b= ztK3*kYdqh?G0Pan=Z~C8o9S-nE?e%zkiqlDlt_j^4UFIUptLXAb)(qG8*A*8=?xue z?dSE*=%$Ap@vhCh2MM2&sXN@dsCz)F`Qy>vI)3JE2SV8=7jp?z5L&1Ox%?%ClrXv@ zhw1s6?VZmH37~U%>@W4W@=HCgIt0^k_L`v8EGW?>aSo&f2FJfbV<}X@-So$^>m`m9 z6i3%Wp^pJvzhOG?t2?Z~f|Lq{?1DKnfh>BZId(GN2Dsg5L892U-wXmtG-S)O^IU~S z@kkHGZ+yj{$#_Z7%`XXh!ksWkX&*6f-_>O{ZEQY8l$ zgn@A}qgUj1U`dlAtfYg7MIINa*te;?eSO`VCw#HY4~y36J#E`5^$M-s=1+cj#Ie4h z^Fuh+@AUrM|5@MAdBXyW`wJ3f*HEmNe6-}-V}novLdsLxjwN@#8BdB`o<_dz;g)?gyZb!g8wGth*q>ndLpd%rGuQ>MfKHcN zrn!cs^kx6Y*Mes&N7mqIm8ZHJyV$tpMpx=Cg}D^PI5TeC*IKZ-V6GeaOjwVg-+vWm z0_h?a<*p!>k6w$#>vQ02mqn*2?>y;)jmCZeiJyqst)*OHF_QB>tq|OxQjJiha0G`= zk!!AXn4z|?+2tXJs)^g?e88=8u^m-OzYI%PT<5kQ=rFNQ;US=g9*za=Ye@mpQ z{mWX~^l!-7(CfOd{nxq=UvOm)j0N{2COj>{hBKFCT*aGZzPC^KtzO7TA9mGNZ@xEt z9rJAWXIq(dMP5SypE%d+OjEC1?^Yk>PT=##EaoT<@^aB=^ixRWcISve}Ti;(3(mcPf1!mUSrzoj*<_IdOVahG9L4dmLr1 zn^a9{P<#Kf?9&^XZe!0fUPozAcGp7=Q+b;u9>B{$(jZRx#43a(s($WG)vTTPNpAVF z&CcP^sJZ-)sJS?u|1VJ!`0ih$X0|a!lD{zrViY?mYg;7MOK!;8_PTn19rR8<@0O|Y z()H$NJt%YK?cDyNXO6$sGqL|t&we?sUx`!6)Y#PWuRWL2Sk%u(nacZR*+)xg>*cRY z3QrpngkjHOYs@VWb6R(_Y)r8nmf+hi$DH-~^W`sq>Mv>A2w?Tr+x-yE*WL#bXgHU6 z-NzPlSSI`g$S#rlKAz=gSK*j9!}Zplua3^#5kM8EknK=vKOvze0042A@M0gm5PXT8 zd?D1{nejBmwom7qQfK>}&`Yka!bF{^zN;!$KU9jAa_~8PN_pg;!gaX}a_ipy>MGpi zhW+hYF}8Z{z)5~~6$L-L3V-v3;D^CK=b;)M!1c!!`iuElF-jBjI|LiLMu`T}RJhar4_z95HD?qHT05LO= zM3Ma3_VIb`Tfc7mw(G`{UbcM#|J?RHIsV%A9cNZ#%$mJ!`)ai!7YOVLX!ED@W5!eR z9gbM>xLqBKlUw~5mtR(X>;-v+yaEIB-=0Yh<4#{Z!} zMpy^`VyNCli&X8%EBs-oEZ1KRRWS|}1r7@K+N+_Wn{eQO6%P1(r2fDaw^X=NusqA~ ziqKsDO4@7L^?QUw{!0z|r9O&=j>e{d-?v`>QGtXzVVbZSnKH5lUv^qQ6-c^sjXY&y zef|EIBVTfV9bpXTdR?s?cI!O|wA{0N-orC#b;JL3Guaf>HnrcqQ6xjR9>B53S_?3GyhA0{s5>81t8*M@@HFGiq78ZZIV7AhszPrsWoZH z2mAuiZSF1Qov#S_)#KCGo#`2iseP~;=9h4LK*XVtUFY; zl`=JwpRL#7&(_QGwe^b1(4pS5B^>=@gi%dQTxK{A-`YG~Ht&i$wm!hU(gUUjuKnuB zoqjvP#Q#fr{cfqu1MfvzOmKwAwM3t&*Bi9Ov2&b%2-Lv5^l@~Gvv>xT^- zNHOj!{h3E4N2yx$=y0?K315pZY>Vt`TY}u|);dDYF-;Xw@D8NcUt%2yutF8n&FGF6oQ&z}!g4cu1ai~dNXWopde{~@;*5zirh*^_2_kDF z6DF;JfQ4EmOmvTM3>tnyour`%OAd8$f1^6{yXUv zvYu)IWg0HLhb6N0Kue3CK~bk9_4de)ajg}aGDol445^Ak<&A?F6Of$9X&+d{Enl4w zRCAil$AL(JyRozPjliI2UlMH8`o|Q0_67uuX9uyGvI$-_n2B<6)$1?4fiQ&=aRzfz zZv{%@xyF!;=sH|r2RuB@8XH_ldL6d8LG0PPi19c*It&>12Sy8oEIC3kZYu%uRJvO* zeSQVeXwuxu=ou7(B31>sPN8B!7B`O6V0ewtFikRv@;9t9ZQpfEW0qU@n!o)ze(}uh ze12hY{kIJMw;o*76kz(dJox_-Fggg*M9>Qm+kP8a7J-FvH6O1N6YXl`sz#~3t2>Sk z&DVRW)}`^+d#T-38uz=sQ?L7<_fiUf-b?ip)emS~hBIAp^asvFAjp&%F{4m46&%Rdi4)j&Q zo?Y`K|L-3BzokdY_ND7|{u>GI-~H1|Aw}|sfBJtfq|E-*hjq8alf;n$jBs44s_**L8Y^($DMkj3DI7Z?Jyv1O+|?eaSQ`=DRiF3Kk86g2o}0 zpE9UKR`!Z)JC^=o&36rPEuo4>A*!pYGi>O#e$|$e;c=}0`Tch=--(Nu9vb*cuC~pK zs2-${J6w5AZ~Ki-KM{yEZu?Q+M%sr%>lx*di>JTs z@~QObsPkK?M!QQKW{qyxrm-IEGrXLn!%W=C?U zjXhG$X1=6C=7!rJgilpi#JwlZ zc4N3!Br}a6&y?@*yvhTE^L&ozA^iTFw;ciy@Da^`SntUecIquu%_J`-;z@lUiMz5g z+!STjWIs3t&DK0%ok#}fh%@KM4-&%XE*0Lz{=-d%rNjCXt$TuQjqrQ-7W}4MI0MpJ z6(`9`-91V-54wKl>4b&dR5LU^fKBIUBnUm@vG!nE;8k?$XVMhQUq%-CZ%5YOT(SJQ zVRR9jN zF5R%6KVB+eJ;z?!r8t@n$5H7QNHL=gs!%G&$i~%v3^?^)7#-}C82z}(M1o?=Qe&Tu zwb(QY`)1Yug!!ti+#i6FHxfa`sgGEQMaDT1fpx%^3+u{awxz>j)dH_$?NH9kRhBRNR4Ge;ukpAAfK4uG;%Be+>H|LIp}rKOVI25c;YY`0$kX zKv`;xzR^@*-oiv%^`s62`J1jmviI@Ahi!?>y+skNXv5v+Xv6*UXi67sLFA!(qTWi` zR82D3Moi6M1nL&l9odttE4hkT9c7c2{-eDZXsJOHRIJ(;wHtcE#iq95M-5(l@U63Z z4#%n5jqIV0(RwnJ5tujkWpwxom3zBSK`VAwO=bMEyH~BP<=I^PkM!2j1>86IttF*% zF5>kurEO+I2Xn09hc>Ab+bcEz)y@&uk{2qJcg1j8T&>vo3R-pVxK~zJbWx9PkKqgJ zHRsz(&X53KbHTN6`MXFNN@{gcd@K{2)8IVmiB`X?&}!u;Vn(D!=BP#J*-F8Ne{|e+ zSzXmD*Pg9sKLz>Rk4y$c&2q_eJ)m+x5cO#b5#K0yU$ftjLT0ghwJYXBr;Rd_F&jH^jjkG28Xj8jf=y9Wb&`5=} zq4H=`hrHcy_Z(uB4|ypQt6DCnd}@5RG@!6~JEW)WSKb4umjsQbbXq+T#gnZR==x>< zq(IM0x~4pDVlf`_V@}bzwBiR>`Mmn6p%q+>zO?j&8S)srC>I?!<|E?iwJe~o~|h~ zXsZO{A_AJ+Xg2#DsLtJt&aL2MScLg1Ei$#C*7Eo$iNM-WA5Z==y4EV^{HmH#W=ohcI+uhgz@3 zBj<&Y83R}CJ^M%JS(|C%0pW*u50T@7z->K^RYmG_Y^oo-4gyaq*SG^E@U43 zQWv*O{bmea&zoSRXx!9_;!%xAx znn@ImI56I?5t-EUw5*n#-C54 zpIRmnD7eJ&0r+b!QiGddXC(m$BpaxjsRkfb9^4y zmfV)8h&(l4(9GL{&JPReL=4W;Rw%0(pg8gKB80#4n_gmio!` zTz;s~DXJbdkl#?uQAc>dB~}=gEB?6O+X`_m;>H-%(8ZCKxSLgqGr0GlW~Zsji9!1N zf;1^HF+s`)LjKX?^eV!sXJS&7sHgB=DN)>0dOCd_r*-uPu_uX3%X>YZOZXXKxn-ug9Y;NQphd=EH{ zYHx4Trf&=mRPj4-ynAqY$7oSP(<{N$qS=L4BI(2;raZ5cu43-M=A59g@gOcQF=Jb< z>hb}Qr+t9wQdl|f&5utp$d_H_Yu%wnVmWA!DQ?CEnt%u&P>zWUA|ldi8d5nlOT!#5 zD{-L!49NnKB{C*Y5z<&YzI^KDu!I~VF&diEi>0O_AIozMqp704k=Z~tEl@7mJ6yC%({O}M0K_!1OLct@*U8vMPYy9FtCL%u8I}-GT zI=?U?LU|GRJ&=zd8J?_yt-ozem^P^ha)$CG^LLiZXU6pJ_7011mew2zY0#7s^!h(W z=C89PrgS|MOus>!jby!gsTqT*qm%GqobzA z4`eN%hTBgrY(DaK2iGs3B@2F2Bw7^@fD~Est_)c&U|vNwfQwQJidh30i)&OMx<*lY zF(5GXpG(8^B>;|WfX z(ISUB>mkz!xp9-+44j;%diGT{{VJNefFp@%KR@-7$0|UchqcF=pWib0W_l!TA~gXS zE7V9aP-!vZ4J+2NC3H(MCQmUKJvAF@YCr_Ant1G6s$6V$0%cQ59UqM8H4$`md4nY= zvz383xw1+u0ao@0^3c(R0XDUi5)auTZ?RdFWS4M3Z+Y{ML&Q`9<1_~SiM)dPBZtu= zz@bRb*dUL8ti19PQCa$Y^ejZ_gW|yFn;)FpkY=v<-Kpc)DABA9YB1Wj*$oKcVREqR z;wXD~WlT`FeFglqXgr|>`O1VyRmFk%Rb5el-0SLIOK{qoK~pIV!Ga*bG8tt~K45+N zt@sF2$atrc)(5sa>|+Z!c166FB4AgmUU)12=(%nMb8J0L6ZgeM!^(7NmJ2*_@lH*6 zTRpdG2p4yH#3Ef8xVGOPAk27CiOZ+{o|6UZS^FAKPkK03?;2Ie(a6B`h> z0#uU?Ljb>*dt=6G_eI(EI?Lk}b4{FU#IV$eJ^K=-)Y}q6Qfo#((~jVnw#1mW%nWA& zoMV@(8VwCtn6QlG%p+EPYpb;KMP}HvPyR7}LI|p6G&nR7z%V@dlL&S8# ztD7K}Js$-=uRu^-n%qdqLh9C(erOm2NeW4cT= z&C4ikjVeQ)>eXvkZz(mIM6?1OEt#nE$;$4SY2y-&BUJ+6i_tT_Km%gbj7saQG@JzZ zq@B+9JwA8NI*jtsfk|jZG#5sioZaMd+>xt!v6QY3?eQm#`QWE{(WH6dl(uJ#t@iCT zR5zdYq&^PCr95Ik`2eef)ppu+(N_8I>R!M)icW>}L42HwCJR_J@Fc{EkQX9@z%io; z%hydNYmUHkin*o#dh|`$BWn}wE-a8_WxbEfmT2TEx1W|@-N}A1zHi77e*axc^5AZx z`yLqm`NW_waFW2rEh+uyiBlbVf}NVskt~OliQOKZ@tug4to?*fnCp9A2ZEhLe!UeT z^qoZ}5Ecjsm+^N?^WT-m|0^{A>$|a}zixqE4{rY=FPE!py5aVrJy|7Iq^L!!Y1hP< z^jAaXVx?3QH83&vCjtOu$n3F|jhB26UCOt>KR z{y>8~A?2;Rg@ZgH(?)oi(<_545OZJLOM@&Bci-I0K>98vuoslJqtl2W{0A*Ox#5No z-do=8Ey!>t^lk33GeZYS36Q%($*p&IHiNYbJzL39s0w)(jN!T5+>9l;C|)jrE)kp1 z4WRHI@VyH$^f=lAjnOJj;|&^0YIl4F zx(-WZ8AkCbC+}xZe>?u5J(~PxpkzGCoV^#2L{(mM$STdH=0LA9;xYO)JhQ#G&w#;; z8Arv{JzxPuo8=e=pS(t1zkE1FjV0^N0|_0j-C!>Hv~tju8j~V@rp4|Si*F*Qit|Yg z9xnPMd=Q>bM53(ryFMteo%`#{+}gr|H%8mGLqc~JW)!nexlY_IBO95GWHMwQrnTzvc4&&4-ppVy^a z#-!$n-XX9x5R(R2b2#*8sbeKP(+uN|$54wJ|+!U)!)z3R3a zXFKIQo}4)*o}Ir|6ffx5pUIFW)VKWst5G=Zp=eC4IHi+TFPS;1Th-rvt`WEptYwZV zz&0=ufP~pb7EY<;H>m%qtymkCQyObBB}DE4|=hUR5iwl{vjjLh${Za;J zvPm_7Fce_zr+eVQeS zG{l=2_L~-_BOy=K^-0gcXqof;^Z&UZZJYs|p&EfH|AcO~%{^nxg75 z2kT);e4as_tou3zZGomI+O@12gkj1mcW`9(SZK}ch??w5Dy_Q2H~6zCtx zAJnP>me&rT!RN&f{m2_X>Dr2Ao!C$dn=8lLl~U@ootLa4f4@ZO_X_9-yblD`kG z5USdJDM9j_cf!MuG4YQ@#E;ct_?^Dqh$3GJ#dj!gheMdC3EnQ(qEzp|tyL_-X9%1@jnm6+9_qLBP@r(?k_v*r$=l-}<`nyj^qQ)aa< zJU9(@?SSc6U^#+(V?=(5(?uv{PI5Ok43b+eez0?r#H>J){^56 zgF3EgS=xHFehMgZ<4T6(Zw;Y`?LfA`;RZ`QqwSHev2e(%5n~;S(1~5y?$}*nxIYwI zAEY0nmkxHwlpy(mYr5L6?hyA;E7M}MTu+kt?8heGVA;| zb@u<5%>RB_LaJhEV=Ccj=VGsFYw1kN{1@}OUPW60MF{mNnEtdOaTyu}4Xm!-t--fQ zN7R-$vJW#%6yIB8DO-2(EOC`?>U}=LM~ki84^KEKZ2ZOMYRGgd1QD_=>!aVsQ=B@u zcvcbC^*+C03^K(cVHP)y7;2*4PK_vvNRHqEUV+_E18M|KhB9FHZAPU3#1JTd4c$cg zNxxGNbDoLRfHT5U6if%+7%Bj^{e#J7ifd7Eu$kISj`gvbX_O&ljKBy3hjZ?zjiWh= zImZvWj!M<<>^*`o{8{?w6)ETy=v{QPGD9ZRU6To;5&bv?PwDgJN^F@X7i6kinyExz zc8Ghbq*FJK*znCQ)F~XBaRau-KpIZgmDkT*$4t91E5?MQ^ANxBEI9DK`@Tp~kml z$_n-rA4N8wY{)I5LfwQh0_DL{{DQ=@DwKv|^L#TGu2VVRM{T%GWCxDh4fPOnHHCyy z+IIsI!<;NS@q;|fk^&>;hlFT@Ca(_Z&m`BBWs88v<{xBe7aqu{lBAm`c^l?~T@08| z@5G|+<2Nj636IC5R)VN^zC?LweL&&sgv+Cv$Q!6R$0XVXcJ_rrM2iiKolk(}LoyPE zBtAnx#^|20`sgWc{I)-EzH#Q~>xn`8@cv^jXQLaxeZ}Ixdc*AXIa7AFb2KGo`irNi zUG3LXR6w`tstpk8<5?42@{i{s4i%*Ol4oHAN7^CR8s}=*D&yKn&VP^b9rR&CpAbou z!27zZw^!TfI}I=91VAR@02KAwa6CPQ%e;eSpQpC={C%4Ti~(jiv@LbA3S<68qQB*v zMzm4NWSD?#5?BIxDF^ZVO(-gPX$LJapnX*^2Q@Ls0g{Vt9n{r>h%In>DF?BzI#kfN zvKuw1^SQ}=L6pp2Y=JSbzJD+oj~jvY6Q%$5aZ`#|zox`8L_H%_Q{G9rmQGiOXGyDt zxM@?3_3Z0)PT_c*mOY)eEP0y!(Xd7Vm2`z|ipQ!$5Y>qk`1bQA2Gp6Qvi;-b&}_yn z#!>~7$l*GKsEIj(ucJOETaRuXtlWaG(+biJlwH%aMDj#ZL-Eo8$0hRo1|G!N+q8ha~=$mx{7=DO;bT^C$!0I zX`Qr_Hrb8%Z+TEfnm#4Toy`HXRAAJ`=s&#C)oMW3(bLl2I)+maf725ND*~~fN}ZQ7 zoWtWxn$@Aw>R| z8Bl6Z3(b3A&o0o*wh1V}1eXFYhIsU4AI&Y@ypBlUVXtl14u#PBByfWqV&T%cfnj*!27Lv@SRbAn^G;P#*j z#LYX2v?4^X@g`YJV94Q%$i=CFJH*=u7RfK1ggxR6^{86B0`4J}U2K6+rEf^O7!6rS zi<0J0>IRCEvWOx)e2yqy_iI-w)4KaF&sX7ZKbPUZN|=AHWrYEDb|#9BrcO@3*~rF@ z$af2(20y>e3)q6Y@OO)}PCfV%iN^TR_hVgZ6R|N*msLuvy!BRT^98UsTXI_xrumAG>IsR8gGlT&|q1T(?~CobYh*Z~&|Y_Nsv@ccj4V`N+IEDN?KmtHRX^ zeJ<4EVCezjJ`Q3@%Ggpiy5+;VRMlXkv3Xlk{vi0ca4hriYtKB2Djp-K!`$k8Dt*U` z7=15V3~T&Z&-uH^=*auJdsWQQ9)?29Jn~aX%HO& zh1l91V|~*N59{5Cgbc^a45)x*gh4jltyuiD7ZvAy#Hg$RwfQBjF$#^~cQP z`+ly?7W$ojqx>@tyq>gzeEN)uHiycL;0Y%5^Bb59%B5iFbX2@a+9$n-jWjn3@h87C5`F4`_r2fI@u{pJL zLLH5D$VymWO!EWjrEDV@dyCwqaT`-Hgy-*rNJ~}^tLHkPGD_BoqpNlpsr8?y9T-u) zxWt14^oC(p->W=PC>cg(U*n7@ld9>sir8rjHww@@Yg@0K5jLtJ+J+vQe8L-QEeMu$!EER(8(mr8)Q zWo>NK)g1bbVLynm!l{X~Iy3Y8@RRE4*yMw#*?djzE_*@OUC^yBYewuCBe*{M3GYGc zOZEZx!QNiTd)FiG4+L$h0hA)(#a1MgoX152)Rg;0CDZ}8`(!{1R529HXIjq-K}^Yu zPz+mV&&LJ@gp9q~T13rE@RW@Kf>dc60`Imtot!~fZP>Vc%Zf64eGD;*T#==y|t(IGN;m%Ts z1zJCyAdOoxct|uIm3dn*eX$Jp}9U(wUT}qr3i4yh876}z3kerBZWLS zO7knMr1Ens=Ri*r&1}rCPF)RLo0^_E<~pFLcw*zJd9G-xorP!tCmOo33>y6;i{sy= zZK^P5yQBnMR^|=c>TUI`x|}&=nr3F?HZ)IZ_-*_KDjXP17FaokvsQ`qrh0ZY{Nin8 z7RPZx?zh#&(bTijDmwX^B&9%R=;SjxoirQVBeFw37jJ3YQR@ z+Uyn<=nfq{Rx2_a!yA3{o0c@Eh8_MWv3w-%X}S>eQS%!!&kCIZkNLDmbnC;owep<%wh$;`z@DHi=%`<^qz+cih z+Mg+$FDDN`a5e9Gg`5cJ99V}ru#9OEv_`ti{h~Q4(S{>*f?<`5jTRaC^n*R79QSd} zx=8xmRJHN?g;UKNZ>6c;vcPM5)`{Q(gy9%LNBrAivSunc-*I+vZS_})R zA+X98sPhME)rs7@u1G#|0Zy2iozW2WDANY@LF9Z1bfj*i9kO;3T~}7U zs*L6Md(U<7YEH{WHcuA$7!{6XTrdjR3gEIc4jSai&@SJ&Y#zeF)<~}Dux*|oJQvC; z(m+1roIilh-`+u<-bAh`v2Csv<{D*QgLKL{vw^>o0hgMktNy-5c|cT$ zE#m#E%~<21wor$=!_@YKjq8vy;>B@Z9ll>ev+(x*H}2BT1no%!`RUW%|3A3PpHi4c z<@FET1@WE6ruEq2*Dm;@JZeMZ$1jBWg#GyBD}{(cal^Z_w5KDt)oV~Q-NS0$e!?ht z-WM^7W^%|7za!$LTwM>?>Y3`hvsUdk`Mg2sV@S|YD67j_stpwf;L+~UQej77_fgkW z=xesJgNXs~oWX@HgaftF8Neh%=^^Us-|m4(!Q0whPr-N&bNjM$l@Qs#m{Wj~tr3SJ zLkrV&SiMB=akg(<03)3XNfVWH^!C&?b~hS3e7(%k;OJXb=QU5N3^RR*;4mh9iYBcC zSN%qjWCDZ9q%jjnqVy?u#v59#Xf$q9px3OmXZ{G-cM=4lZ79GcOEUraFQyXPcHUo` zhyr6a^pRbNsGYZoP+n3G)!xJ%8X8n+N(R$Z(L^0A8P8o8n7V4mwyQTDtDleX*DTzt z7wBIH{g%V?naRUF81$dKc#dp1C!MC?xoS#xZaRnyEIACBtj}TWGR~KkD`ujY3MiMC zwzEmHWO*9Xu{>SA+e(nAz`4Kb2e`>bl`*x3l5DsNk!d<}_7cFNza7Z~j^9H7!KY*T zNd^awv=L9oF9Y&eufJ*x58dwXy^%U@)}lXZ4uA#U#hnS?L0#IqVzJIBbLqtUCvU|v zqkv=)ni8ptds7~Js}R=(*uWth$fSAlbxF*t)>ysI@Sem!=w&J_hzW@~l*EY*$;7>h zg9xPRQcTS$gKI-CmG19D31qM9kXEUE1|G}~+eN2fo+5^T^vJ}~3@w;>oq_1YU&s{V z;qJy>{O*SG0l|?)w*C*h9NzvDoc`Sw7#PJmD}w?6nCY4Wcfb- zh@t!^g;51vEUb+`N<32wvp=NsGI3J2AHER$e&s~(iVXl4+yRfL{x%RvlPoOmlOIRg z^eY8CqxAHM9cPedmYW^)U<;B?>XMtI!t~|`_*VOId`WnD!R`!QV~|K7{|HL)t!l;M^sh~ zOWuyYYE#f~Xa8v0*xnJPB@$$a%|^`=MuD6Hk{ja5IrIzp^d@e53qyK0u&;UgH+)v# z={*@h7 zA%y+lW57``Qd|SSp8_VO71j<1Q(Fv9SMc+^izI`?BB)vbpdfUBD8?IS#mD z>?S$xO?ltkuW^51N|yuW9TWy?!!yG>!u`V;&^`!0!VVPS)~IUvE#&~LoTVQtOBf~( z*C*ERb8d2pnXJ}czHhgF@l1%^lD2Qn-;?1Yfy@nqwRTDk7T_5yb>jUq`FRK>JBNbp zs96pG0-Pb}$w7t>)M)rH(+C<%t%%bOl{8$IWjCx`tsaA^m$o%pWz@aM(R2hA&9{u$ zE3Oqi6@t;sW^Bq89f*L5;?^HNtPGwc^IUC0XG*f``^tuyC=ekTayaHuV$4Xt!`_n6 ztcI>=oJqiIyIP!413ksW6YSJ(SGO?En7Q0%4|pb{Ud=35!H{+nrB*K0c->4ZoTCUI z&9tXUYe-7VbW{`>(q+(@ay&`=RV2A@lTXE9J|{atlU|TKUXgJp@|~L^x%s#I_tEL@ zV_S=U9_wI&rsV>@-RUlv#SAyA{GBMem1wK|ATq8tnUl3Tiii6Pz|jwt>ncgppR}u! zJYVM>Dtf2vVt5_9&1s*F5hc=0jc!ECGkAtTdFtD!`IH(9b4zz1p7XGF=ro)Uw>4wO z)uav!R;wV~_`qpvSb=RYStR}9izRH+sXv8|*|QO9cKfBSW-P^jLxa80_r5?gXZQQ~ zphD3@8zuPNDrPd){=9}A+Anc_`yw+Xzc}-0-QvggRRk^%Y6~HbaWEK@=nN#LLQ~!u zqO#jOsfnmF7ZHJhu~{&6xQ*F-ie87vddDPd>=R-nB~*(n(Kj~2UOB>^jMzO2=4;c2 z3#G$-8ukgTxL-Ccy{iscZu-GkEdyK%dlt&>7^?1UxL3H```+wgUKBebKUVP$#eiaR zwZg$z>@qWO#&~{DC=#G~nE@4>=6=Fd1)0K~K;Y zfqUCJ!Qt0|9G<>^rzu~lBlkXil+tAX7;+{4tA6)?Y3s!QYHH>Wakxsp=Py_#v(jZ( zRn6+_1(atg23R;*5tLT{j{mg(eG80@W%>Z|YRvT()PsT{Yi`g6B_yuh#-0h|_`%{c z)HkXda#?at)Dl!3|1kjtRjU1m(FN1V7NjOce~!yh^mwao#5Jco#BKH;8P{h18@cLN zEM|>r>1d;m!_m#lt2jj=3c(u-l5iF#%b%JhP6z`ikNG$nMGVT-i>;TR7sV`*cW zonxSJrXl-;eXxQ5Hgz9I@HHTZUOQB%*BF~YJ95rB(||G>H#3rGWO^*t@X%;!VtT3; zDvce|edrUf41NrCA6l?BXoNfpIX(6EHTc<;dD=FvP+DGZZFmy+nGva<9YfX+6m7{Z zsBswRk`L_6wM6Xdc%?X*fyIiYURigM>a4KYU5THU$B|3#I~ya5O@V5O%S1V6lfuM& z`8|_Wn`xVs%ESVrTE{jwe1zu&E1QeiL%cy7akZ7K2%A(4-l6apiGx%Xi37dMrAFx? zL&v?&7sN+?Hr1(3VNa=Gr()F#8CwPNY9sZE1g(l%{ROtrOWC#z4f|h*=Yw*WXGi(> z$e4#s^kY=B$^*>QeVyd^oWDNzF86kPdsv&@xwU5C%Fsi^pG3o`D=H$y8OYRG(AYjq zx8iE3LLuN-#(jmkqvX#$jvv-YK64kDw*q}?o7es^?OLVEdTX$WsZrUzYDig2sYh(q z`e~|?%ud}iuS?-)7P1)i50B#=aLljwlnTJIQw+Z(R}m&V%jh0eANTEE#jxlqdm{(e zyp~KF^!oB|h+N5KXTS+F;45~B}Rb0j>YA2khx%?K6EW;rJxvLB9 zG-^+RZToavTU>TA-*C|8nucw6e1pjKe|syU*mLBLPA|ZRTDWubS1W-yW+B8LOMlZw z`FI5%d#wFU`v8Kt!n0}zXyQHDq`mHVBoPxkS|n#aFt2Js`F`(G;3BoU<%_Ud_9a;q z&!S(4X;^-3e!Q)#LC`LYjWFqEnn9b#3~8O#PQ6agdpS>x40G@xMxKlLlczRf2Iig* zB)-O-$w10jm-fvs9=#2dFacY?&;rR_#4ho(-QtizWEP!Gqh!_Uxf61Uw*f5n7i=+I zeosjzkY?zD4pOq=HyGOIJG0OTw|=FoLp=6N&?}`F--=h9FJ#;m^j$qE8=Nj?f#dYD zEA$c$&L{kEczj=n?#R2Y6^{yIN!Ep=pZ!jVMp*MM&g~$){4T_(A6ysB$=V~LqIicc zoUpxj>1C0xn29|GojjMA0FSb1H%PWmw;qwmf=Kf42Ld>hznm7_YWdp1y<*Z;^7?{mX8AV=D9O+%tcP*JybAyJk0GpeNGJ7~9nG?BiUjfxVcmSnkp`TEA zTY(>eNEE9*zrq0I9cB6a4*?OGc+2kD{%zK~QW~%M{w}i`EAPx|h&>(gVeciX&xY!_ zy%LYW;yniKxFLo7ao;2}if$}pZ#XW2CUVi1=S>J3UePS>j^m{%L(zQPZlY(ev@4Su zx`eKi%U`mbKc?~--d)#%&m?~7{Z%_0C~YAZNc{WO%+0$$qiO;3k%t_)~ehS+Y`}>PVfO#%q%jl5X+L-Q*lZ!S6VJ z3CVek0xZl+`66AX{sSQt6%Unx#K6k%C zgbmS%L>uv${rQEsZ4phz7qsXb#5kQ_@;UuKb%)`1Lqu&3O8Y?7*W!$LLN*1T?DmJ<*B3&2&Qts%z^ja9-xatm%6}d6Ew%=> zF#%|yuJ!w;m^o)KfxQUf468@x8KmFVPgI}n>(BTq1!evV5-8~it?lko9DyI2c&f~Z*SE!ol~>C}Q} zuD|^7*ec-)@gt#R{$ENc|CNx|AN;Z^wnr9J01*66TiZv7xHEvU#N>=xqOW>#{K_8N{S#g|z2OdsT`=wnpT+a5MR)Ww*tf3$G*4JISnnVZ0UqWemK|s} zCIhUWG1%@DT}@mLyYT0!(>ZEMEoCO>11O$!F1xTElvRk3HF&`R)W7PH@Sqd&H!P>L zHkGL+k{nzt8raS~y^Du5e)>j1&-tGI!Z@2!%iZh7!}QLnp%MGJV?!h&4g<6D5C%@U zzp9RcSjQ|xRgS>uJhgrU{?`|JMuwR`^0Bze=_4)tpAKw}fA|8kl%$mAz$8*MNcHulXKA8A zJHtTCWG8kWB4FzW3DQpyL=OYrQK4?8{7}FK2+M&6u*d%(uY-x)1_ztEqhS!PO95kv z%c4!nO5YDlS8Jw9uXR~uaG98^SScr<_Eo{?nywDsXHJV~Iy!hR#%!xA(lXRBPtL9L z*kn1Cwzlh-4)os;c{prOB%x{DoaxcX=tHSiFnb9g= zz?LEkK*AY;YZkeCQSGP6{!we5h7H zU=42o8%Eix>B@*K0QuQ~x&9L7(j>`-;FhEiQ#(6AaNkMz5w77fMf_6jaJwdM8w5&O=0XeXz#*Q^WE+O{}K9LpnAr$#Gc2go`^9)G>voe~u zFU?b^B`#(`_4`~!T!8E?RIoD;Rj@~KN-<_GP|N=1Z;`(DF(b3Aj~XfLA4U59CpiJ^ ze{hv5rPaSgZh5efxmZcr#9gEdif8HuUthbdULB9O-y@6>rZFiTcAPbML+Xu=XuPGV=hs|Z)d2QA~YbZauEt&)c za3F0yoHpEBXAc4a=N8*s>rdF1=2?)n?6*t3o zOlwyJ@Qud~6o?6fOX@9~?qVDExpeWfKSSvd+`BK5HR6$KrCVzYSnW2YGiAI%!ce*h zhCDxOd;Z2VlG&#S?OD3@1c?bzM04#UPzep!4M$05{BK*SsoQxtXm0yCQ`RVjMMm<18e zs1oZma?Y%gm zWCI_UDvvR!HTc$>_2@9rqjQEtRk5QLlD-|_hB-EwR@o~*rj`@QG5i&mstbyt~O z;=U6$tB7ZH^FIB>L^7bMtO=WPdf;Tx5k*X%7pc3sC)wIX(=;V^s5ujZ$uL_XsshcP zCY+2|h97EGi!V_=oDWKTs>_BsgE1FnbXz_yv3YaFQ}w<#j?#d(XB;fI+E+JQyThHh z2rkQE`S*p^--kRkZ=$7~`zkf>Qf+NAF*eEvyw;GS!ra_zy~MBSg);Jcu^@fS6RM$P z#PwYUt3Oc`V;c0y6?5{Dj@VFZ18>xx7^8#@(3bmR`rXu1^OAUODCCtx)eCu(MiJ{{ z`9@-~8vB^{Lz2dQK>s=nd8D2ewjY=I-aleI|Gi4Z=nqjUOG!rwSpn#S0ENCpAR=5p zOFYLfsissv8`&!_NdaMVa@Jzq0@B{pu0G+s8gr<|K4Hwts&ub&$MyuKqjFwtp2Ide z;E{8`%gsGCwtaoy8S`n-o+|JSF`hshV89$WCZG*t$QXJoAT5!`-Vu{{#Pul+?85Q< zb#_s$(?I+XfvCmzW;iiB9%crq7ZcMUGkS}AvTdjpx;l@}-+~-z?FT-X9DFt7gHrQ4 zeOkJX{6w2!MB6$&WjOt4a#;#myS54|vw{=rXfM)e2b391HL#6|g?(%~PSc&RDNqr` zx$028O(D$@^IglgA7AF!*(KP|T?1`qP?n z-6)m4ZOg|c+~5Q#im8d=EJVfp`#4<%`IrS6=Xo(li&;fM3oZE!frYFc)>M!sie{qD zWlsFiHNTRFw2cu{!!$O+v`?`y^K3GkoKvuhHhf}VrAhd(>q09dsGc1LRp{F$;h)=lMi9*QdEWG^hA)!IpqN z=fSNKDW{8Tk74c@RUZtSmT=*OZMa(t(9aB4(McrYT zyda#s&?3B|PW-_gr!Hi)uXaB2!5*fu*d9ceA(-aAcd+%u3;fp+0yiHaDb$9%jpAY~Sa5!9;Gul)2!6qgb)9^N@?TsKRUkC;y3FetB zIMpunE|yEVmP?1ThM8R3E!xS)$!nj3@vI5FnCwr${$)hjKzd+r8KP{6Jt;IzXcvC4 zKTwqv>fyc}rEe;6gexIdjAQZZb27LA`4Wa`E$v-}@tFcl9JE1==_IZv`P&D%_5^77 z!xe8wyO?0ED797?s+gMym^fA2qz*7FUv|HZ0Y+N{icp-L1}P5#k)92j(O6pVd{ zX$&&!lZipQhX{QacM`6uLqJG3dhEXHOoQL`jiYt8QxU%yu8kS>$u%O2heTN}5!Xxi(u_Emx72w9Q@F_2FZc zlutsriU+{Eo0o}lTJPHp$s)rf7SPB;^5|x?wj~{gLnW$4(P_N`!JCbg7N;7Z|k5Z*CXs(xw<^p^OES%OK-5vX~ z1CV^FdWJW|)BEW}>i|tlp?=Ndu*asB^rErz*4`0O(H~4;BaK7QbOTF}u6(zplAso) z;G6hAHuKZEg)+Ovpx(dPz99*{pol)289!i1U)CHJzkn}{Fhv)Z)C2nz?8Jj%3dvWb zTF~BdLKAudZkmaHzQ9|eHlzhawHDsRAxxXDyj2RtWj#2-;*d#^NYT0`dfvpI3LX5N zL_C#SWR(Ah9`e7ML@@usks1}Hq`(*u-*$ekpZ1A)|MW!mEQPJk%|^Y{pi%x@g?gFn z+UM$$u|;}(KAW)BgST~#)Dc!M!mTcs50+ruHSV=(JUsS1$Mg+8Q3n-!oHNyl{yaBP z2vQcrV-ILrJ{D!=Gt?P6_QNWTJ+X(i&)Nm3W6PB2^W*q7;AV6K#>Ctev-0&0M#zB= z+7QW}$TI9CU0x!1gszgp79|f}XkY%(-(W}Gm8dA}Ogc6D3Ugl$B1ZZ=6l()jQ^bu> zSE>jT)`ddE3+PZ4R6Hw6KHxEX9{S+#Mo2i;sEq6;LL(heRsH*G`n`)Fj?>W)Oh22+4+_}}|H`g6mHtpCFlg6%(-PyUz zj!)O#)BZW%r{jmi-J}=}yldW#2Y0&Kw?`?W(G|ZWn~Y=PSWecoa4@VKD4O^(tAW{( ztz4|L*`V;lkRO1G$-N+eW65l8UZs(J4Y)19x^wv!Q{8oyJ!#yIv%p#LA#wVEx?OV{ zMfK$Um7*hxv84XG1lDHYrW<1Lj+W?YSGlrlDeI z88VHIZRo-TH5<}tMIuVg(G`kcYde!ME40w*aOP;}Sj6WGBTKVr&nk%G0`$+KBF{w2 zrf(}Kvd`|ies*A1s^NJDD2`edp3*RQNq_-}u*Iw$$vA7h*HM-6`ny{x)J{Fv>$>HD^8v7JbZGKE@eCIuDN4^5N<=tZ;eTp53w}hdi^o+jdnotNiyo%j9gA}yNW zbt(Qh8+`wGk^Zj@{IUEgM5s>L%}FA@CAWRIQiR=h!)pKmwOv6a%R8c*on?h@P}D@E zfQTktCWRbAXbRxp>Cfp+a{C4+j^EkG)dA*OHkP}cIZZsMThNcp!WW42zn)sY5GSG~d-DNVsZM3G}$1IKLB zB4)&m=h~*(+r^RoGUACQG;4j##EDTWqIDD2Gs5I)fD;a3*|m3WvhLv^r#|T`4yq>9 zSfKv0tu!iV(1u`#7v^Fy3j?ZZnX!%Bl-s>meGgf~7(C5_ji)gq^(*0M%}dPP0~zKL zYTqG9B4Dfxn0zr0guIpJ%rXsy*aac|NOo6>yQSmQtXNVO0Xdt$&K#${XFJ@NAF@iCCJ?k)tgg|h2Ol`ZYzvg_Fgs6~ zyVF>T^i{V-YblG9TkNWUlF6^>rK7IN2ljsx_ z63p1#Q8F2d7ym9A5X?LR9VPE{^GdE?DPu+ghy}Z_lh#dCd^fm3iROtE;oWlo%_G|% zEZ#9RB69SSp-~zyAVFt+=szcG1e)a6frWg8ranWFCEKytU(CDaa zw4gUYsATcKFbSB7NYkx9aCoeLoR<>&&s`D!R>o4<&e_0P(8Ain(Z$fh_75JPrSSK_ z6c3mnhzE@M&d(NG6rDCpk3dqDNM#hYiD_B=TIqzeGQ5M{&ld%eLJRt#SnF#C9QTiR zZ&$Ee$Yd@UrYrpC~v@IoJ6Pb9MGhSmstuZQ>^;}Ne^X*Y!PJ( z;;`Bj9^AP|fzp^Vx4$#0PguN*!DaNINkb@9!YTZ9YvH*l(4@?&NifE!{0F(mOvXvM zS>U<0shtCcufg8WHm>cUR}6|ReqSbzdG$c_Xk^-0i?C5*NPSglXuL&7p>fTH19R-+ zM?ec3ci$=98+jxqR#q~zGxi_ZlsM?d9ex$ddu3^FSDHY#LeHqn>SU<^M^hHC|As6( zBRaQ~{AgQtam?qXI4nf;Ra8x0v_zAwgV9p7Iox{pZ_(QHYy4-*kFbsXkEj68f6iY2 zYOgG@lCz5;vB4j#G_e9>kFp2?x%FNZon*4Slqyvm$n~8Dk2rB!ADS)9zttMd5A;Y= z)7n6TaW*}Jc&#b}F}0ZBW_0hcMM)e>+)^?_&{G=LnNy;~VnupR)|_leIsw-r!y+C1 zsk_lws%b4U7C>LnY`3Xv*Zt0OI@%W`waEph&xpW90?wGq zzvdJ?pKuSo#edOjDWWWI6kc@~_NpoEZly$Iyb%|NQ)M~QoHoC*3RY!l|IU!lW*Mwo zVP`#85Zqj(qRW`2v}U1db!)CQUAo}ysh>d6Ox77aJ{R!Rcpk)txF=6S)ONR@`%8hE@${L;k>dFC<_NC^6e~>Ky6DBA&v!SC=UU~DHAJ zMT=2g*Q)xWqC%|exHmXjpaGVk1l|r>QUSPSDGW}BH(6kH3@3B`R1M<%XQ-=l8W^^_ z7wU+yR30wD`&PPzjf1S3CVQa%yR>oVLEeL8dW4G6 z8h#4zZq#t7WY#qcTD`#=JQ3j7n#?{jT<5&xR0+_!m|!)LINA~yqqqm7g?XDU;+pj$ z`lDzv^nPV(>6E|7B!6BYMqk)RN~9MrcEarRPP z+FY;2N{byMryARZm^1ZFjQY$p3q&-2=JVJslixh7z883^?kVtCb;eL0zE&`nl^s{` z=a!a~bx|O1tg%d;Y(omc4RiEqUAXD6lj$ZgaqFIY&v3VX(B8`&*QpM8r%+>(qPnVh zJ#c#`q>27@IK&E%TZVtki02pJj0Zz{G|4?Uh3@B_Hd2bo>3dWbHfKh zAM}*Z=vfbe2O_A+T3igXO14M+EePy;j)mMgjUBy?Ur=n+HD9;viBqGMPvb~&j#!r z@CHNIQ_1fP>y|>w4EapM!PE&?zDr~GJ6Z_>}5h3CE9pJ77^NT3yvMycx4ui zoOxsy?QV}f?-v}_BUk4faIy|9@qmj7&fdw@S3+0}f8Nmo=9~z^N+b++i1Hvt8<1 zl19sdbAl2sW7TrDttwkKlF}DR#I^#J)=f)-s~C*K)WlfiJ{h*&K>HrOOi$lUYrRSR zlPwlXtjPC_XN;WEqq47)byBjtjREVE5enesv`dNH>`4OT%CPj>6#e2R^n;Wl7vz2R z^^BlOsBGm)hU03Y26>F>T}FpoxW`E_=BlKG!=8b+jr&;Nu(?^*e+o^H8~>{J#=bKu zO6zyNel%rx?lWikLtK7c~-WUEqZpemo0Fnv6n4##<7IbIh8LV-1-bQESF94*or)V{&%>QLYf!`M zvS(1kulTK>aWkB{eJgltH^AELYOGQS%iy0y!T)*KvqCUw&q5 zzL*s-+8MV>MdC7*j>ol{B21>57ipBW$f~)J*-c;I=iJFHu`9fNI-g-WpJ}?+wTjOu z^Ay9xwe&UU+zZE?SC~cDIJ0jLvz|eE4da9!)5IqI#A53HvGg^3u}7T6o1aBj4>KL( z1fT9qLc3Zz-u>RBZMqf%-Lb`;=k&qH8{08WxTWsrr0%y%U-J|{niW4{6hHFLy+F;q zkj=e(v+x;a{`l;7gYUzli_mxg39*a{1tL z{5|FHFCe$%{{^|(ERiZ82qgv}V!+)}T&ay#F*DQh&$L^vdqDNfcl78<3ByR+oO=FZ zbF5hu2&{jwIj*x7-KIF4%npWzralGRQv}u%07C3}1K~b;K!R}je4%mpqDd$C?_CTG zS1KdbW&|;gt>-=7Fun5j?2-C9*a{=urO1Rxy#z@+m(ZLJ%Hb$T079f5;4D&Rp9N(0 zZ(ZYyl4^05EYStct)n< zMmLeI1S_B^Gn{Pts7;9fnqmMZIs{0P9qwnU%wqx_8cz356M|6F3_{0Ki){}mdmWrZ z42;iZT$309LeIeHJQBOqC+n)V%GGPbR>6tEw=45TqYIlFDt`F1-9A)QHwV3zYuP_sjOyT{?loCDLWI% zT-|M^{hnJMs;>mqKE>1hG{9m?#VIPhNk3WUIGV#|S!H|QCx+XlIk%<_90Mg?i#(NinrF#6+m<35yjV(Th zF_&@$PTbY%hPr-Ag+`lKHdQR~Fb`1Hb0h~*H~hN|_QOsiC6>4lNsHen-*D46fWsSh z)(eE^gB0LeCh|(HrC-k|yj$S&FS1hEYzJbVED^|(4gyTl*nD%mZPo{0LRrYqvzyVW zPw*28)27iZ`|&GcoctLU@AU{GjF%y(oI=Tx$=WA~4ma_qLW_R`+;=?#sl>#I>&$Q)7cP6_H-fnJQ5cWO;_^dBa2zNNnI5*hXhz-GM^T85MW3!nk z&M*3`UxMQ>qyv_tFtm{yyz|_xx#M7y=mLok9|ecPv3z>cCeb$SEvTItzex8=B*jO38 zYrSjPG}EfxR$*A2fm)eAfbl_55#ehiPY4?o7Iblx+eCt4ihRQt-g?Dxt|J+K&=c~R zvwtq1Okr1xXrxhvp)B>+ha$nw3+8!tJ;6u`cfZ$?hFz^|vhs#7nXhn(peiu%ZZBok z`+HykD|cyt=A&%=i{0>NcG^F!{{45tpZ_me7CU1PQG-88er75BWh>N7h(ZVfr0GES z+clXR(vqtgegB@o!9b;Jb2aj0o7wgBhehrOuos0v2tNA32;Gg7E{}RU#^YHY^J5o5q)&`>M8x(*~1y43HkF9MT_{Ss& z#s5^b(nPzjX3B-b&`D|_5fsi7uve)bnJz9y8J2!w$ME@vM=E zfbj`~-Id;+zRtnCuaTj_p?~_`sf<~|TTFuj7(Ayu5H!7`8A}QG9Ziv`!QbML3E;m> z9O?hd8CfQI=JnJ%gji#o9v0lh^Hrj_1D*#-=v-1#;Q{l*C;?b!z1&2tJ8(LMjx45! zv{!YZ1KbnR&k5|Cp5aa9iBxtv4xY#5eXRlyP`whQBH5(cBrzS6lHw@u4B&k)=OSj^ zxLa3v!nY=xw+{g|6Lz?)gg?=Ru+XJJzp#ryJr6cb6nshAeBG~?yo+*G;sRZGy4xxxkV~cPFNy{ zZ=&&yO@ymgQiEYh{rxb}(u9K0bXX`JBnC(#jLF8~+S&yR`cxXH#ja`>C|h*)yMsYs zqz;M_-~0nnusyUN&@#&iZz9RUM_nJk6_sb7WoN&29c8Dz{@M)T`_vM7OAjCGFM$+K zNGQ^c3IlfjrNt88>IZWyQ!JbOmmdt$l9&FdN{ZMqJwIS5ij6{vF#GW19Wn<-*mZlU z9JGfNu}!o?3qFJs6&>d7!wIa9DrgEDLR-NiKoFvb|CX-L(s~V22VOaH^FM$LYRR8+uV}Hp@w^gDCq*w1kt-<_k zT}NX4fx`W9lP2t_X^x8Zl=*3-zTRiB~dac;QVjAllyaP0&r?)sdAlj~PHYNXMtnK^nu zie@yq+Y0>lhdgEQOuUuD*Q~EJ7FGxLjmcFD6ZN+!_Is77T4~bT4l6x&NS&7G~^6mDfgNxJmq&;rYhfP6E9gx6v}metX>|^RkS*@ z{Y)=ZGlNeK${@W>qmBRC;kA6o7er+o(ttDA;q^TH>?x-lc*5p+!dtg_^1Z;gd)XLbc; z|2%ph`E2`&=+{#iFGWmQwH*2JRJfF)N#>!W#15D-^f}Hf8Iq^uYB629pOM^xb0~A$ zw)3n${s!>8bg>k_pP>b<6cj+qGtxu(J(v+zIh%AubKCy<#TrP z=bL4?zVbP5Cr69sDc8`dsAi8;C`v#-TlE@NikvdObv{N3AgtkjrU3BK9w#mEn^@^P3*%SS$FmFrwOBvW~NrL9I z1Y!P8Ap(3SX6BHE#H5LSK3~8wa8mzE0H8xz*GN41Op;Y6U90k$h6%N*_LN0>Tf^^M z-PD|ALPo1)rkI4#U?XOL16hrStUUPu=qVL3)!#&~BVaM6u;m_AUiqN+Y!p0C6QXFR zbdYYh42ZP6dC_oFCXF}qBdFiIho3^>TkN%=C`DW4oq3xZ5Ywxyp-`KWo8#A_T zTQjz8+t!F}Co{>6ZJRUJjI;Bu^BvTwT5ErMSM92C@xL4wt@Yk|YtQd#LdSh&X5~Ww zx5M`1%9V$A%Hn1oX72n&K~qcLH2UFA}K9s_h2C_{C>F+pQ9xVSE`sMmdIQpXr@JJ<#tnuA!@{>Eihz{o}fmiSE z4AU~9*Peb>MGwqI-e{1S!u%8F{w}3`2ystFX*7)z4^KNos$;XsgxKK-`+XG8x}o+L zMl?y{Nt%K$K@P;McUeexfm<-gP>SnMK09fQeJLkBGC4{){Y@~Ts)W4$P~U%ozM9T- z*Y~@8)%cGC^Z&h|_xg81|9@K#l*XTFO5-cx<_}|*!hj1YDha_*!Pt0+Zt86y|4VpQ z`AZy?ox)BN*FQkUq$sI;t_RnExp${-D3?d$yaUu&k@LscKb*z$6}N@m@l%^MCoo%) zf>2WsJ&YTbrOIp}QB8r)00X#p@G6d|HPWTf1za;99?nlL?*rL_%Zc0l(A5{^f*#Oq zR7=7aE7{f=tP=$faVVPQ5GH%=YBlKZ>2o5KNlTPdHH-=0vXm0)Z#S~RQ&ANxJxS&i z@}FVYK9L9&+90xu~()X*KE@MK65|(;?5b5e$mnMJm zFI^hI-`;6Tlvd=xA8tf(&N{=(zrsPV4RSb;m%ZwP`&*cXok)oBEle|KG5JvEHOZ84 z*G#IjoKlguyV@6eDZUN%sZ#*lIG*EI0IZ4FIq2x92S~P-0>$)#nRzO z#^!Wm^!<%GcM{4VN22dCf{4MzK;znQJMphFga|h^?vh@(E@R~i^6c8qw?0@LJX;3p z0~Jw;#eZbw{JJZ_=nN{A+Ciu566M#1mxAXH*a?l_2_C;L4if11u@N!O3S3gkVPVqp z#+DI(W%om;ohj_@L{X|jN$rSk(at=9Nu)JKewY%L69;W#qA4EFc@<+J<;q4*#{TTU zo=3ma{XCCeQQ|LZpdPaKEFK4bnuXep-#v*(-cvw2P*57qFU%5<;PVeO|3tBUNAG;L zez@mc9oZ4SamPBkRyrhP8W7kg`wCfjXXQXP$niK@2!>F||7Ex6C1dKcEFNV?;mw?K z9f3>*J1+p9C@C6kX;3IGv{9G0_USaA)12Shx4$wDniRWJsK`g0n(*$?qc|&^QGE3; zb4QW7&wTLzssDh`fA7uzUrUDn@y%5*{@&K<_HFC<&+zl#Bw?xAh7+m;5+7+zb7S)^ zEC*L>$GVhDQyQ2F0}B`vmMFD6jhm8~zA~wm4gF}>^i@=U*$w>jfG2Zm2@Se_LDbHu z{}6XmQY=*(=4@rBw^!#(&Xs=b_UGS|9Uz8XKL|BcOj){&c1z>Ap`=J_idu`r{tOJt zMkc1hVN7jVGb3Z&=mRZN2fA=F3i`>t5)2DE-2p9FP9|$3t>_Idvps}qEzNdCYM3Y~NpNeZG zbFKECMc|UpCUU6P!bT%#2fB-AzmVXpmrYyhMaX3uT&Z3c=31N8*DO@Z7umiW2|>cp z`1SSTrs4~3V?`z5@_ZHP?=NJqz{qyXqLOsH73exxtOwJ?2@m!Si$q)L* zF4qs#Kk-wkt_T|CB=WSRw@VHI2cKe5hz=&N;14H7(~&G#FB9jlFpm%23~vz)D$74K zwu99e)1H`YF3`#*?oofsn7SjZ3HqsWqiSKI7SjFqE+%VWR$vzkxGYR)m$-Jz#e|UMKaug=i+kc0O8B+C58$5 z$85LsQ!LE%EJ@X*bPRjY{ZX;qXi8@;)B>W{_PnqIP^bMXQe6%i`a{}AKB;4OZO-G1 zOhm`(7_f@eNm_2p0Apisrc3h4=@(1vI{;XYemJhQ*A@+B(*7L6Pl!#)G#{1o&O=wl zM-EdTW$N@#;>$jO8-)CBcw7B`s$R?LvH`z0UL$T??_(V+u!pix0&AtK)W9(RDBAPJ zRYzLy)o30pvf3izN*2n!w+=LtTWhQ}VnF%5uGXM`jl0gNlywD^{wrat7<1|k&hwAD z9M|4M)Cr`zT8Jpw*WVJjBQDiw8wrD(WnTY<>^o2m(Io?C{a#zmKgif{)b8=R9Lx9a z%g`#0dOLGxh3rrVG6n1r>b9?qW7@h?9L$nBIWg`zq{JFnaFnCC#PNyK>F%?t%g-D* z5|2H4a&Ul8RI-Ih z1UI`wJ$;A%C*&&uY);J6%*4eXNJqL6Vr}u8KC=q?|g@ryUxg2B_vO(0X#3xrx`VoOT_bc+= zeMmlUMrhM@Q79%E9phR@g@5B-OiDf7otCU6RpY2|3uTSCG;$#)zaCOX>RtYe`U3om z`r<*hpjs?NQ)2&z`YMeTjEZjGK~~Ck7yd`Ajpr&xFBv4Q@u)ZT_5+aD@j!y^QTueY zwVkj->gTQ+RrplZd3P#f{~1=!YhK&-C8k$9n-TkyFC?}=J zGIlr2BDks`@aZoWES9se}J#!|JWlJ`0v#?|Nj=OhMARx<-cu_C;tmh z4I2T!uw7`Tfd8J>rLYdoIw9?kOX1-ep`zgy>1*LyWVb&G-L0#3<6!avA%u2g0X?jKwluQQr;x>(}L8YdsO$M zg6GlP2nV?-xVua=!E0Q{)+p^{pVkrspT$B#ZY#QzTP_z`{D~tCoG099y=^FeNY1ce zo9>-uvOu|j1Gy@rGvO%aRnQvRhm%0p2}cDjmLQ<@cK*YQnp=xsS8@*73l;1vI* zK(Nn_30rIA4|4xT)o2Eesp7QO5vfmNWM&(qnla`*3c0m#hN9h3*e+(|cPbIr-1xCD z#zhj%a3tzXD}TdNj;Z9s2P3tVU$t9OwJ6LbLF?OU>&boh5gJ6TF0qf;9!S#Wv``>= z)92y-CTX#J9pfuseE)aMy;$mBU1k25pfH{vmiUHk&dy~s% zdwJH%nhcVGn;p)}&&SN|&yj+4NbtYv@p_Vcve=!o)q~QV-PN5-0ty^?_{@YVu~a91 zn(lV@CKyF!r#6}5#TqPX#aZNHwGYo=H43*WDx79?zdZZ3W%YPOb67!J4)&9L$0o*qJgokgJiz|n8P30?q9T^|HfGM> zF6;LH)=K4H!<_HIn=r7@L^#DZciZ>ijlHylf}MRn!{g_&8|biE_J!StBf&fHC)I&6 zJIZZQdS-6s-{Yyt-d?^wp!R+%NH<7Ij4%vwjNcd(V4!_y(kg}5RCQPb9_rQ$>y+P|DllOw!r|o}@-t4T% z9~r-o6^j3OtZ@CW94r6N#*u$hRbBde8tSRFe6F8RaC5e`EnEWSicl-gvvJ?grp+T~ zaY)IiKmj-B8Rc`ZC3iq8Dh+t%iBZ!KA-GbFFE(p}qX(8L_{J$H`RlfPyCRA*iS-ao1u6wMp&-I>pl=*sJGMz?M>wpFj5h%+w8*Xdb7iiia z(Yj5pezsD7r=$Jkp(U7z>N88l))Jebp=})J)-rQdw;H3_XdDNxe9ocauZwz9yA7Zr zP#x~8*pIG!PNMnLLpM+#)=<4wQ+p4m=>3Zo1v_cbb? zTWI`>qjoCy-7BAaXg&*~1Sl+I2r z2p^b~U$LUH$EGlp;PNqeHGq5)WQ!Ban(||^{NhL5R(&Y>X1rWVz{#mk1Cd1b=^maf zh6;CPS~2|t80FXQVLtcIB0?hM7H+!B^qlvoWC4de3weTrLr3|~I8oJOOWF@EiH*~6 z?sa(qVG2Xth; zGg<_B&M9|Py9Ay5$L2vX#=g>ahV?<~UU6v?j!~(?}!YeIGgAGaxv^$DxG#+I#SLT2eJf+P2zO6AX zrELybj*G+mm=q$VhSQysynZ64d&I`weu9U;zE9Tm73zw^owPjv^q9r{K94aZrN-f~ z`*8UEKCvS|rR6)Hqm{mofo=jOVtP{i;V`1d9br^&_cQX&hgS2+uz?sWCFAkoa<@kg zuE6x#$gm98i=jU60P2TcHxuJ0aqW1tf0V}2uu`)Mly9OBTOHKxe0PzdJ~`>raViX* zh*HOQz^7eJw&m?JyNrWvv$xsmXgGSu5mNct>HRYbzpFF&Gmu{k zjqOFV)*KvX5-af<=Cyc>McnELJF9lMJr5hb*ct3-I_3x+KhY*;X75*R1Z>lp+Cp=4 zd3JQ=%*9#imbEvJgMp*DeFZy1XTjur6YhdbQ-P<iD4ZW&I5=J2kgqy# zZ>F=MS(zg2xk7(yA<*1iu7w@pqr6|;*makd)_7*u)*M2&9UL`II5zEIU!%@4EcnEd z?}N(w%IvSlUBWR-&&Y$!8Ex^njYywZnjd~cYq0ni-MNC|=Q^&#AINRiIawW;=OL}g zc)UiHW@}CR2r+1T=~{7$@#(=27gTWt?1kZ=AMn9{Faq_yJ zNy1yD4U<%=1B$f!9)|w<*}TU3`ttd1m^l_S3FP+s;uOQlIFRf7l6o4du;c5Z71vRlJ)Y7AOBV9VvNhNeXu8#WnY*CVoId?^VIQN3dOb z;zc{iVBX4tyV)#e9sSj1oVxsOtHNhM86*DF(}}A~nryk-SqPkA_n*VxO=)pdJ2?6X z9?~^dsxOM(MDAOnMZnoPCbj>7V- zt!-^hq%c_1xLwHvlVSBlF zlBe@0ZZhNwwo&|8Af~@c`+&!^v)9%ticgQR>8dR&Q-Ro`-s*C}SIE$B5J6x0jrJRp zvB!l(s|~^N-DT5EhEjJ4s#tc)MM7~icx-4b=$HfTF9=d64idty=npeTi$&B4FG>B` z#X@PxZYViH%p2ct^%|$_6f2wFdTA9os2nT%=HARK9c=^s#I3N(g7MUx59tlw#C^_? znEr?W?$_*qi=5tpI=^;iYmHV`u?gNrtDMTpoN)3WJMd3k8TS&Cj<95|UX#LD6XVxC z4vO`m;*A?8ybWgE4aYh4b4}1J?2N4;LN`NgN%(i?OQM>Vd2I$q@s-an&4#kKF~_6P zq!1#$t!S;h_SdXH)fG0z`YFXM`D*M!*%E?3t$R2aKN|v=mPscPq>9G48j`tSc(Gr7 z>;R}C6mf`RewJ8DDB58JS+!N%f#^eFmnP~8ZF&)4q}i?ZtJ*1CiI;DhF;?xp7VUr| zEF+YR$o$j1GRoScb>;O!BkwdulT0w=<(SAL+Ehd2GzQEm;z7+~N0s8pxnFK2u!%*fY-~`Q?lvoYLLzuu6uf~P z$D+1y!J%w{cs@1=(eC0c7QitqZfCnLxgJOh`YH@c3OKJJ~rB5{rDUH?x zk+V^@iRVi9+5^kNjZlOs3NO8oy%7R>(L2Jf22OK_I9#@V_tvKZ*{$CPlFUDBf_X3& z56Kh(?P8zytExo%U_YBzgTs|4q5~bU4bB=z{94dO^}c7@S<<7<6%3YMJDNv3>_3VCFWfeIsMpSi{TM0nIli?(RKLKu*cL{sq0&WBiD3**io@g{Cvci)v zhazjzD*Pq{M(XDlno*u1&b$-tf#~TO5d!OLQCHJ22IDmoF@@W+a9`&Gx}?2JC2O+Bn4 z32LY7)Ko7A(4OISUH48n_2h8wYEHroYML0{cJ}84Sfy z6!<4VNlQFNOLuDU+~{i7oaa~60aw6pD;%mQ`!yz(85q*RR7H&pYor(E@>B~ZL7)Vi z2=)ePA7Zey0u-MaCP4)kQa5bIl9QL}5wBfxO*f)KOCnSbO@;l;Y88(565ynng`B+7z&Xa*uxue zt#%y&s1SvFCgzx}1ziP|Vc5e)rj+$OoIny_Q!se+!iWyCniZl}AnOn$5U-Y&42OqY znlOXNt7bL_)kHm1zhPVm{8$78yk+`5{g>?xG8M zE;Wk)>W3u@8rU30%{$cp!wnp!IQX%{{KBK&CxM|JwV?holm$Wz4Br_=fT=-^kO(Tt zLOT$Zk_Ca8Sy!Zgj=4K(?^z0s94?FB4O({4j{XLNzW>U2s9e1t+1yeL>OxKfGOM%p zDx9jh0cYr5+caAW%)ET0Ff;kKEaKqxu!e(1Q+Ckxi@;F{B8-MVUc@*MO!#=FAQo}txShbL zG0(us1mwv3s0mmx5DaG4yXtvmI*GC*t#r?@IVIp6V9LO*bBSST1L9Yqktd;UaEDAj z(D;xYz}zP4W<#y9_n;cWrl-J#NcBWT0dND+84@9RGqZz%5b7qN3bXL={rx^75KM~r zhCowJA{SspDV7ZcWD%8uGbj-BD+<4WegsQ0#uS7nR)zDUAx^Rex|uw3pdoCA6QZsu z?S~QR5b;2yAg_ktdt)rfN~}ZKiR=h(9;_1ad!6D)oqFH_1?7fvgl9oKCxmLk++tG* zmSh=L5Tz^n6+j>j-HTKc1mgVCU`uBX`ZWsepczrjO9h$(MpZ8wYsT%+fwxnHhzs6k zdfD9I1sxLsKa;lWh+;GGJ4|>IEvuEb(wZSkpDC?oMH1x8q^IoFnQ7;IBHcGmt2A`~(WR)~=l?q|t^T zjm^@6nS?I;CAiyeWzA1bk8dzy!yg~iQdEcWeRU-VTSXf|iHE*RlEzbDVY(eQb`-Z5 z5v*+HIv&F(54MWObEdi7s3{w(x9tF)%t1HLz+H z90pPz^aYTqA85-&BPmL8$#QTajdo~< zYAB%w1zM>huDc7QZ8A9el}293n^Q7q?}Lm?oMpoPd=iXSTQ^(vQ9@j)u`Lu_7O=-A zS4JmY=zS`=XUY|!tWyS#00)$6&gBL+(ce(osKE;8f)^Q)nC(Q7yD5%PAA*MPsK9}y z6sY|Z9m?UbjVNH*QY5w>9CBH!^}tPmQ@{qK?QojdinB1K405pkfpwwGK%6cvxFHn4 z3Sr)91zC9r0#}8}vHgVjFAG@C8A-S7bbrEmnCb83NPmsjTaGwr-63?Y0xyC)`oPT) zj`qveLtxol#1)!JNiUNq@Z36Pq=6GGH1_);u;$TE-o%~ox8K14(x zs3qlTedh`z4sR{p*o{P)7)t7oW^=ow@J}#r;zEhLBRRW8B|%~icMsJB3xlqN;DFqp z{2UBiCZPirnG>Q%p<@eMb8H^WpAK~5<`5RDruBU=)5=%_;symadXq-s5Y8rS4#M(; z!UX11lpb=3KqD8RK^znn8QpIW3|$49L%k~mj)~CA5Z?{UV<4A=lE)29`m=!p7oERG z`W}eEGW;M+NKA?lGzdN8Zgwb06?g?2MYJ6-$mAg5M+1o~$;B0_Ca4djNjonLn*f^^ zQXHF^7zo{X>&5Q^wusDhIovNi(<(v-G$aSYHgHAs36+>n{t~PuYT^b5@_S}0u)8&% z=oqYR7%ehvciNrE8nuhng)su|RLs;9+Q0skc?zPQ>^%ZZ!t4!0IE4xXTpZ7KL(zy5 zgyt-QTnQoN2@A3ZwZLZnI36J6vxn#dwC0?0`2}XinxE2`IiuA2C6gi-gf7{iWOU2P z{=g-iK#R`^taJautg`z9K@x^UE5z}g3F;8kDDMFub1w&#gkzx|Ap%G|-mbt4Y6D~e zkqJ^W$)c;*LYz5h39S6&jx`C3a(_fBN>2N5n805?5^FAwbOU6SILmd93q?FNcXMt8S2e6zhV@ zm?@s*TW(B*G`pCdh#szpL`0nfrz=u0KzK8Pgdjlb7#LVS>w{Yr%Ln^>^}Evr2_rc8 zJi^R2VU_I0WT#bk6l}cQxqgNaQ7jHJ^Q{o9Bbes_c~jV0aeNx;=1RLE9SlO$a3t)H zq!r|Q2pj_RHx3#b(T5CL1jfaV4~p^ZLY0j#yd(mO%W4a`|2PniiYYySSpFPX1((Uf zPj1FD_kAPTeEk^b2)y<=PML~-rd6vA2@^L&TczFPF(yTw!}8xel7IqkK-l0YD3OBl z3OwRAO5vB=N91l$x5BM=-Vwtq8W=a4MR>?5#uAOf@b8&Uf{uVNo+N_-Wpry0o{2N; zAWpMbO>iOG^VVI>wjysxZFjWkN&q9_IwK{yfrrX%L0H{u;^yIxpa9_kb4atpdVhw( zg#sZJelQaegkI@0*pMfOdCMIDS5xzOSH*ui_j+e$Fm{*{w zS3dPDH?d0>;Ba`B36=_wgw3F2hv3^t#Z%&D;xbe*yc@|!QrXR}#UyolF!lq(3$6`< z;pPWXgT*=(Pm)`g7>Z^>#IiM!0G0%Rtpb~o?Y98|g`A=xr2-+4na3J370e}4#*U7_ z3c+$@Edo2WgJYc`a05)I>w(*boSR> zGz~lcaMo4cXO?kY%#S1lY{pB1(+ggm*$Za)TBsj9`>!~%J-3yTPj{ALvX0y9zbV-G z8Bb4B9Oe8KN&jKY^EpJJF%Hfo8Tt$vrSg>_W{b+G|0uyS*)tzA$3(fg6* zQm4_x^zStMAc`jC@!l-hHqKJUGId0Md;HeTb=Fx-DZk-h+$YGnyyJql?u01(%MA{g zJNJjnVJDRh25R-GRQ&qAMruTCOAs)sWxCXN1_OBi_}mj^#`X4|7Dwx7qGI}hBjXIP zr9t%^96>n+7B2xJsPKs9;^B0V{Iza|J!7#Mx09(bOx ztFTh0?$yB0ICZisJ5GO_q6f93S$#MqIrz z2$Oi(kRXas{4GZ<&%k%1F=a-WACL*?H)?81v-nb6F)tM|I)pC2V6WHLS7>;WZ3kh( z(8Y84#2&P=YRf`!2d!!7%X-^bPs8`!FE4gBq5;REa=^@wBSe5+#~c}+ORgS|ozgg_ zKuYAOf~A>@Bi-kqvJ;rF5^G*x#JlDb>5o)Dr!6=k+I%F$`yENoBWgs)aK*N zPH7?ByjV_R@TI)=Hs2(({L?P$psf6*bOsrK&#wdaCvi-WwcU=1JeO7BMaxlaXd-or zGz}n+gSh=wx5tBV^+h<;$17t25e${BxT};p6D|T>wVcCjY}wOE_z1>_0(XM{B5LTfER{S9~iQkG?Z()5%*FC zME@_DY4%@zsPX(DZl{ZBBB;$+r?iE5bhxfisA&WjC&`KIKeHD2zdwvkL#~DZJMNTk(jJp3EkQ6tU=vJn+92m`l&iRbl z!*pzu*ZkpBeRgnqsyNYXQxZ>bTZNOG>!dN;(Q3aUh&k;dS}b;e6UY(g^hioU_r(P% zIp5HT@#!0%x5z8*lEv|~C3O>NBDMk9`Nq?EICL2@5S527K4>;a!JW zL_*+e%uf&e_7C*?Gfdjj#EbaxV5;m&eL+PJlD$6;!^Nr0WfutQ-G%R2oXiYZ}v5(AsBULlQ zwJ|nAfqYNU+QD+jb=dZLu4*rBB$+>X5fN8{IG7+2KDx#g!s{8^&%%(1yOaY1_g{K} z{&SK3CNci4)KdZs{G%G5d=$FQ@7A(sEkykNm(L@q^Y0fnJ5|=Z&|e zuFdT7H&wUv7|WYkdzS7wW8C zY>RqR$MY~#zYusW)=($vse8)9wR$k5^{jtFKYgqf>fEWI$#Dz$u@0LWe!AmX?mksH z{=2CX;%o=vj5hc|jerIJ{PTki+F8{>=va{EmjY2w&N0ac-q9(4mek(`VXuT~0ArGv z0ifnV&5OV{IO;*JGpkpOu`vA9_JMC*VW&)Ce$XYtPtcvzpK<)UEPpQj6glNR$4@AJ z-ry9oD2f1`+e>t^z#!W5fyWVy)pfqa-%v1B9eTwuFJANy z*@e@E)xUgf25|FM`wGoDc|^gVVFX_#^sBS#7nN#ME|H^(WHgp-cPHJy5p=Fm zIqmqxOC`9x8dhk8GX7I_Lhs;dVZfuxX@d}{4^R{;$IR_y^QG#J6&XQkDil9o#vAA@3d~Yh**2B2f4)@ej5PD60tQ@gC+IN?VkBxz zvE%X4Qr~GEN&%Tx)o@h!y8y)bLsGOB%wzl_g&l9mp2IMw-mv>nuM4vTe*eMQxgO*f zD{F`|eEOqeU8RQnv~Ll8)OhA1k7T}OS5M?ynHxKyTc`-1awA|KY-O@S5>mJ0ouTuN zY0C*z?qqwRQDp0{ik$ZXk#D5~jys?1wKcAue~5Y;W*T8pTWecMv^VZbpAUD^ikuFQ zWSx6`k(fejm=^k5d8ZpP+S8Ls_PC+KrJVbm8n`2@+yVAdA$0ndn8v2q$D8ypy2$WZ zWo4KXB?HK~GQRD%S@+LgeNo{0y>B~}TEuzx0H{EueYl3u=}b2GCo@Re9u0MmtuE7FDc1u9@Oa@Ny!%m^G9qj zv?wTut{cwi3|>M$t0<=WDk<9>?KHBlrgh&c34~~JyRa_ZQ! zL2fq?hmblY5RQQ+KI9pFTibx`Y4s4CISo3HuF(t4guW?5Ov(^E)>DteHRl}MEq@lZ z9auq69~}4i9NKmLqMla2NNK`RrhEEIXNlWr3}#c5(Oo(`Ivoy;p)3{k^u^+;(p9>K7jnhg@GwYGai2Wg90(*3HqNxU`phtQYQ+t~-{y$Xe8GVbA zhLaVKeFyd!JfQ_^tpz>(7mZ!v6>yds#?axsUDn?PEpYHNE0H9eodOv6^Kn?3g(lX3 zLe~+jHRSW3bv&Y9bItjHwxuA}ZQrqh`LPuS9@%Tc#PHv|0I1R+dN#FwN=tUO6LsZO zh5k6ZGOKZXmrAFE;{%^*MTEJv@v>3HxP-b*k*XALtL!_i^%|czebq-)FRi7SvnjRs z`Hd<+x$D|jc;ABawujrworRk6KA9I@-@wc14zuEWu43c6LV`)cRh z`;X^89h*AE@f260c$Qp^6}O14)H$ORs!pP3Dzb+(7R7JzU8A4#I^}!_58hQ@Slm9? zFW$1HL+@Nmm0!Y?6?;>0meVT~`HrsH;Af#jeD|1a-04&PFO$-wHt1O`j=NPYzPM*qD`OHJ)`_G@ zcf$m$7g*^kwxYqv*&t6CB2AKAtxM<(a0%pw1{wS=g)HP&vu$ak4_3ziY2ZsL$1U8o z@rMN#VeX_=kQMUp!mrj3wze7e*%bu1t;QEsV>}E7De|X!1`?Hs3ICq_0 zsVqy}mb80Y&6!~r>`T$!6926mi4vc5mT?i)QlESP4dQ~$5#($ip~x3OVd=O+L9jrv z**K%5hM!dH9j$ipdmsmuf6;xaz%RMa%vP$tLW&ey5iQzrw}NW97e2k1#tFq%R5QkY zcGb_UycdFT{9$D=t%8|1NSQm9RCRtK)_4{HIt|`0DnoQNq#HA{lZv>C5~ro3qsoZ| z8XDReJGaF5!Cz*a4Xl^KF4E>^Fp3L~1ThxwwWi$DW`7gP&Y*IF662VfB2r}Gk65Fv zypG|rGP&`I9L+nEsV^|hjUSR0#A9RzPrj68f1l(>&<`f3BKnUsXL5PZvADcm()(O- z&M!VJBFI!k3qz_b0Wie&}x64!N?r zJOnv?3RfSZY(frF=Jz8j12DXG!g`hl#OTUYSRdIqi|5X;bJNV=oLDhxMypGfuw!zx z#Qr#JWjLO8$0%egZ3n%BQ|-k438m@+Gn*Ol4QNDK;c8+?RUuSs`cN+s+A_1Lg=s3- z8eC%z_p7c|WwDZFpNk02cj*r9Q_xk3WAu>>rQ|vSrYU-P!eYt8{o~&C@jn^>i%3Qeg0)6rFLZ9A2i`i%R?cS0|ar>^| zWvZU%4oB#v(s80sXqydsj09Kf$~PnGwRp9Qu(Z;tGU2qCY2U$Mp^mW_HKRD6m+75x zWyL3JV41C^<;qG=Thpd^-d@1egtsosFe!O0C{PcGnyj2pkU#n?W-XMolS*Wp)NU(6 zd3V^$86R{p*^tDaz{4tbGE`C9M`Ko?3)yTk4%3YB=tqjXneYU=Gj1qXsIwjxzj-HK zdZYH_LTm4rMZ{Q;SesO1qi#wEnCpYxcV-=1=*!BmFqW=QvX_|EhT`RW6Z_9sw3qi81 z%neL-ok|{Wx!50jbqX}a_hxhi_NI3v`sH*)5=?E%_RaVRnz9mNu(Ci7QD-VdcxG-4 z{hFj4Ceg*T;3r8l<)=AUvM87q%8c(s&y0^$qtc0PjHTKLX%yFOGrf{s`n+cg&h)R` z+@iS*F$T=A;*>|(OJFc$2z*f$l{w1I(C|S~V+#GW27AjNN4{*=isLE}>`kG*Uqt@h z*aTP5Br{!QN=;fxQD4j!l93jQEru%kuo4$}dS+s^z;^_Q9#^bdW&r)`xh5J*D7?tw zx2Yk!_+SL)sqmplZ2PJ)6lRi5P=tt=;m`tl1c05mRaUco%RL|nNZHWm-E1Y<(pvg8 zafSC6ip=({9V#rn0Iu$-1(m%=K^Gx=7qCMugobX7NpJ5&+u9E^V{8|h$LJey=hKnh zOMVK~5{$C5yzP@wDOFM(%QQ6tIOxX?zJ_@E_>G2 zz+2^ss3&8%+lB*ZFZt}Idnd`LHb7$Vmt>6}k6eu(E`+0 z*|77!b7D)7r!Pxw%BmmG=e51?&z}vdd$3@axT8QI2f0YJ5gyD`S zw8moyn>x$5_;`C?c25$&dK|MhTO=UdkY)kg7&mS$Tz7S7;1SWi0}U5*p9{m*e4cB!t$V;Z6T&Y3Io(n9ktvIOk~xa94DYwYpk z?$griZyU(+;R$R+_O=XmUx=@mRU|$?#Y}!k#fiIHOR#pD?jc!LYt7+#CVe-)7 zmR{HH)tBTM`rcbQz@W)rd+E~Pfmn-K_-=^)l^&fV!$| zuMyTTDsL|{79aLSZ=xZn^jHK%4_0z9l8iE<7B9t$CJ7RSBaX4m=r@DsmbLbXafJ8u z%o#z4hn4)PDVk(NmdfDNb^b$r$$_`H-t-@9E$pxv8hz&#G}m8U%4c$6HkGqN8^AOr zJ1jAPAHxZ}gBAN6I|t&q8#>}+?)2WY{e#YFwlA=7mFJYrQ4kG^8~c;J*;1mk!J_SyrJ9YI#`}=wV|P`3 zv(>7cNQ{Mbl*Hxl9`@XU!W#1xY#R}F5S{#!S+w2#f=5Q{&t>&vYrMSHpE9jK_>Fel zpAsEdcbMg@gYTi8qJ+WCRD>5%YTE_#2ss>?1n$e-%ErUo6 zPXU}{WhuXVqqm`T$q#WZSwg}sW6gUFiIP8JR*7@5?2;~EZB7bj>Hf}AjYM86Zz#O7 zYIm{bBfWX-qagH?v-k#)AF4S&r(HM&C8b4-+lDsR<3_jlgqY1}47)s;V2M%7h3cShNlKbzB^ z*DFMIm2j042YItE?i0Y{h_i{_GQj@`W3T?r!!D0Ks~Eq9gHtath|?_+B?SM6siY9w zo4q={2>b&_TcklF3RHPkEqk%Ws1MCu#?b(c882=BlM2UCGCUNr#G?)EI+3LKKq*G* zwmvT2PpGsnq1-yaUfgTJtyNj|qbm-E%{@)x$56wR*gVUoCu;)&xCr*mjp+6R;AsDd z7v@3j!k&DLF1YxC(=qx}+;@fb%^u;y0p0yV70nc!icedspZri$2&N|2>kYs01v~gnjN19g{}Np=cz$jzTA;BqCyVKg5NIbT8GP%G9)BSS}wH5O7rG>f-<_T z&kY1mpD(w+3@IW^PJ2Zr?HG9@kF8GJ(KUMcnjRh%s9hMnRZR}wUkkuwduqJSbhD8T zcHrymLTeJPTUcpt+ZfqdQTKo(qx?WC_=C_0a}V&w=*d*K;Dq5R&lQFStL8SRB)FGm zuh>Opf(MAZy0A|mA9_%?*&?H?gYQSeQ3h$~81w=zD}~J!T`OIxh}l5f#iHK~t9jV7 zY=NIC_s12C(CP+bahKH~|4)y!NkE3OwA?`HfP$lZQHi(uC{I4wr$(CrtO|Kx9|PlzPI0Q+#Bz{*oZ<@ z{aN{|UuK@nb56wVv95mUk!Z}?!ARsj;K*~5K0<=S`090|8T(cVh}TmA9i5I$Om(mj zljA)3w;UN*x$0~q2vhv=1v9bcmd4o+_u0hoSyQv{T`*ZfXiTxdFZ(~4$6{#;3nhX` zA*_V*L7Wu)S)SBpS}C0s+GDq^x*T&M^R2Eccz@Xb4)|GJS4)O)q|N*EmZe`OLODfy zX2<-UbLx(Ocp01UJG2viCYNfq_zN25ZZNHA`AJ=&7bQyTyhsu;^h&$gQ(gZETjZsF z1lr2(2?|*y{$X{>)Ergzf!M85#cR(9sh5z>+=3IW$(qq(^@H0=F9CUe_&T;YQ4LL1 zj#uAZ)Tp_FF1{Z8<~AwmG+==)@%OrLudB62At!w2NXqOz{>84WkVa6HoA5;)=^1^Y zvZv9SsxyvH%7K5YjZMP$g{SDE(5Xp2?asDjyApHFL&=s1)F6~i{nE$OsU|=q(=f9$ zSoe_)CA3S6kFlb=vY$gP3h=Hh4NR&Agl+L8)Ff!c7#)q%c)bFY20d07Qh25|{e;a&>7q{DazD}l#vP4yj z@5g%dQg@yz3+{?+A_bs#c!-ssaHtI(UMuOC#Z5Az0rWWh>W^+&M%q-cAe9o+$;ugqx*4N zo~4h5vT9(xdN?1+J>KFr-vhy4v7>fy0iYica1nF3G~t){xIfJHekT)&b;BQA!dr)& zJI$B3gexIiCe_FaoLP!gO56_@vcf|m_U@s|mHKo()*){|LL4@rB z2FhPfMCkk-eqjj25%K_YY-tU+w+ivG=rjIq#&7vHXaGNele>(VyByBh|6?@GE;1j4 z+vy5Bw>eJPC|FU(|Er_|{$K?IB=k96s3e~-Ed3TfMvXLsPM3<`9$sLao+UbJ6D3{~ zemD6qz3=fK*+DAbR2ap=#d$2DyJvnaG^_9unap<&x8f%$P0_#-MN5Q>w3XU<3J`xEjzP!pBrIDyzf za^B?X-eiZ{L*+ti23H@s6-VLJi1Nc`sk)-}T}Q_Aeig3gSDHXbAo8z^-z>nFk>OCU z{D-)rr~xJ%FaQDofy)sGj7vTM>o?1eZfNwCcMm(%rr}jIZ+s7ppDZ}XAOQw6EkK$N z8kArd$_#y;w%B;un->%j0b_+hBM=^Rk1mmRNGG$$HyoKGv1U*wi8s9m5j0DWe~{jt zWRL!A>=6rhn57RNUDchSuLtS-N4HKm|L^YY=NhLgp2 zoCU1Ujh=~fb(0Idc9UW(LW8M#@A`fVw_5|RTSm-T^yB$facn$hdaSfI6GlDrwJRlU zge@1wBclbTZHzJ_<)IF0WFQ1zE07m8jVu$8DhoSujWSF8kNr(m0_ppHGMs;M7Os(D zBAjD)r)}8nVR`~8HYTviF8PQ`)g3v-FZ32$j4(!U(h%m{ zh}>8>DvU%=m!QSB#tUE_+=SU_NHu>ihGkZ2J)(>K&^W#iSfU4?2sdx+vAaR|Z9V?k zA6O!gwP9ij42YIlul?|%oyfcm3W3J8mRqj02-e!Wp73wnT>{Dmw=H9bWJ6BDdf#0KCe7V1d-NIx z<)1;e5!b$ANnL}TB~a?mK)xnB@DBQsO=}7qhJ_+y*_AmAtOj8%)M_l)hwj?zyB{m@ zbt8O=erUJ6c>5uk&D#_QUwmB^i(as)4rYchq0CW&{vHab^G-xKnWs?}kOq<@oG;VW zhVKAjfaKG3F*bKxL0Y&rvpiFga7z=R`HJAv2y+b#LUxZ8Pv>^v>&3Jmou> zy?o}g<0nyj8S?LL#Q&L?%3nJ9$g4{u@*=$#W?VEj@Tl_e3Xn!8u63Tt8r^H}3TOctq!c z-)9-z9Uh`ZAF2-pM+QNbCkq|zO%qo0Fnr`ub#DM`K?vT__&R#cluWRkMG(P+Y#bM? z|06&gEp}}H$x4qU0HKz2Tqpi#>VzfD)tLvWQQd-CpXG6<$&;nAFlyvZOJdH8ko*>7 z0QMOM9^7-EOhs(#g$ggLvLV{=VrH;M&YJJ+iH|WWyP@iJ*ZmOGXu4p@ZwI>pMhQ?M z#^?8P_2`gaszxG|?2*Tc7CSG6(uElO$G-%I(&_lGk)Y{r=P8CXESnl7tR*d01XNy{ zs7)O(dpF@S3H{h&{K~8fci3JE57jHjt#eyTdJY4vH;8fYkU!bKc}E}#CA z%uFV6bPg5Q6~UT=T+|4xf2yan@NyZl<*dVV$IG=^3kZLy<(ceqGa3#mbkbRQZk=;j zH=c|=Z4i)oTDbN*gtn*t5%6<1Hz}+kvW1ARf;0qC+DI&I0@cNVSjn_p zjo8|`+g3d0*;S-tswAg*_0`vyFP2|Az7?ktiNysqdzg3^g4ns3_-xJu)qj4_n)w>B zK;I@V7OzxkN$}Ok=^*W?GsXT>7K^{b;~|0>2x`v-x3l=OL(h`Et`D91q?w0OmrN^A zln$4Yls4z=j*&@lE8IT9^H_hm14t-&)Se_Qz^h^>D2gZn*Fubu;gy+qck%-sEt9vtaeJrbFq0+Vg4%;ss*~`Ayq$1RHGry*GDOn&JN$Wd?^BH6XFl2tc3ZPywZ-Eo}v)fm^aeGOa?lji6@_*N1ISx|YR!Mg}58N{X@}xd58)L&629T`Y%Dp&yzI z_ZgF)JPnw;E@EupCYmL!gRLarph_RgxgBsw2l2Yz;Y z!n&>F&lO!7Z!S-=(zOY5x^p`988AhngY;pb%6yK5;!>WF1Zg$Lk7u{xsNj-an98=Q z!}W3qegO2?c40tvDqJeO#YN^gogs($JWhT+AA`X-%cIBo7rvI9}Es7}cHA;|d1U}YF0T@BE zRufyAuAJJhLObW;CE^?fbEQNoD*AzH#^ zj-p)3YAX>*R(gp$x=~}SWPQ{^c`%p`$-2!j+nTv|YAxDC^!q8kvtP8e2T#Y~N5 z*P&1ebrkflQHiovJ5>$Yq;Ld9KVS+5qwzc9(S5a2o|PBrQZkl9c$?`cMcRORtAhi&nRaGwqs0Xt~`wi3*w7 zUnt07w>Pqlw2xLPfdy+n*~ug~+=a$YQg$6P#F%5z#)BR4}f8*?JATEA5iF5&JJ`dr#|tYJNIxm9id z&=Q1Qgzq(H31DV9lnmd4DMa%MJP)(b9*nBync9|#DsRs z=hfRqJB6fxc$gJg2hIG274l+;`p_@64$at%|E)dp-YpO6T2GDl>6>r4*Z7gO?Jr`1 zCnnkVcoMHcm&+f%&MCzYN7pbt?c(*9Ye%j!O}u+lBB|!l`R5B#a5E5-Y$-?;RJmmc z(BgD@_@Op=-wDbGMdkyL6OO}`=Flth>3`0pZ1X7h1F2aCUHW5Z8>hh8*drXVsHuFU zhrRpacl|=~3V?aV=y-&FzE&5wW>4?IJ^Bq)crBbhly&q5bg-h;ofVV&z@@&G@d#0I zQ-wVC$eYiDL%LucDnRY4Eiomk_GTOO)1v7I;eBx8^djQihlj zyC23RHD_m`M<}nXyC`L#Z+C# zk6tEn&ikQVo`eJ@WvV16>9)qHH9ivD>0`n z`nZ)OH(T_cTAuEW{*%nlF*U|ouYV4*io%VRA|C>(6|AojaI+*^I($KlG zy1e=l@;l6t4G)+$P2jMkMGRc;KsH;XJ{}n9qi=wJH8fFzC^!y|i z%Q=RU-_t>p_4F<|u#!WT#y=?MW^IjMr&Dely7H<#NZu9`d6FxVOmuSsTcZQky4gQ9|F%D0%(vY@ zHyLfVy4^rKt+&5}dYWv%u`%or5|`Qv1p=XKCF%}?10QA-3uZyRNzL!LpwFLSO3m+; zLn*+1@@|5za}k9J#$=&JuR;+cu2^R%JSTRSd{GT#>U2?in@Rc%(S;0}@%oaXexeT= zHed8oj<-g`Q4qq9U5(M4^jc~-?f`4p?BHwI?GS@CUwv6(RB(29USKKcnVDhbr0Aty zMa1qAv|qU$jMehOOK2cIt2d;&3meg-i7-BwTWj$>Z#5Juckq&mN((7|;9DOzcxDfa zzFrYDpS)P2xe~Pfx6jVI8fFa#m-bHt!!%f^)J46|G7_99;wWCuOli&PlKD4{3LlGBr7kR83;uU!_ zW_ePly{#%z*~3nATo#T?L9MT2SL;QiJSP8LV48o08e6BToH)y2OW2gfu%-4h?3o+*pd0NvhwuizY$^%ZyrLTS&wG(mYiL?fNSwh2+n)%ajsU8c$)qBup` zwMzcdUK3|ZfB_TE^|%_&96cK?&doSslH4cR@S8FRvwn#dbdX<-Kk_KmgxayFZg5~8 zvbyy1xprgV;M?S7e~-Y7P5up56mE4`M9;5u^ennL+7b$FEqhWCrvqq5#2S;{`a09e z48#!&*U45aME*X1%gGH4dU=;2vxb%Dh>JK_dq>3) zgFs!$buMd$l<+}H>v*>d?49f#1y+i~9o#Ml<4Lg<+G~IjcT`A%zvU+B4I91|x;<&! zzEWz42pf7BfIdcj6GYG#Omv%aAK3IhL4UP*diBY{&Qt+e8?)hT2U#7UFjeB+XMiBe z6VW8-mc*&7OV#Nya?|nB3Yc4eS>J8JOiZW5 z-?M@VK-veidOz>klA8?%&CZ;~CRB%|ODVF%P&F$4vdvH5C6u|Cq$Jh7Y$a5G=#pWq zSE>iYO$$yw7U@)MF6A$CpL$7g<17H@nuDClsgl8$G)f*?^dKHuHEUh1z9 znAg$)PoYC#FuK>}>Bv}T*8p7`!D_o*J5ILDL1uKfj*f~Xb0PH(Oq*9DW#nyRnz_c5 zVsH=3i<9CZ#yUE18^-=tGN<|SeM{9rjcb+95r9bC%5$Mm7K#^9ATM5xZV-6 z8GT@X>+pth0qtP5eaeY>Rhh?$b;a7+2y8W^&#<4J_p`~b?!xhtw`S>Vy`2IVROE&e z$gj`2<}(*%D{W99 zad}jiFjojBZ7YnBfrJabZx1QPnRy~Xt0AZ}-Pc$Zhv+LqXVkP^QFUBKce)sFC0 zEq=lG8|NB){b>YEXCBPZJnFuL=9UlxWYj8zp+TL~6O>Gz*AsO6QI*>RjiE#nlgI0r zA@-ePmCrKy(39-P;U>l0oZW?bG35k{(~$pK(ZDJ7-T_EIo5?rQ?x2G9Ih0;JV}=sS{wV zF7OxV@f>%f;xpq~d%2Z4@n=hHX6)^njQLm!pBpn#y$}r{y;jvo*RX8gXRB^%yTv;mq>S$Wv`uuTAYlo z3pyb~t=SZFAE19g0kw^lkbR$pLp0*w;b8yBEbNa_{(nveK>HUnfCBk* zvrkxzbIyWpg^|d#K`G*zUN3i>3=e`-Finl3T)P6PUpsE&=Su5}Ps&E9bwL+ID3>?& z9%iufqDap-ujGIhXQPW0`a>GGu8-GO=x$Dp!{+XpgXUq|kUhE~MOYXPSmb6#2AkeM zHvW>eHDi{!jpGhznEsqA#F)re)IMNzBacAL4W9fMCEH)*dtFFu@uFSKuq{lmEv6&n zr?wczm{5~Eg0ViZ^++kRP!YtY$s~wHdB${@Wx*<$Vlf7LlrbTirV4f>cb0DG{FcO$ z1vmt~Xr^e5zA}>T_(Vj+$X-@VGwfa(Gy?tGQ?ZUt1;HjU#Hxo>C05^T$+8aPa^|ZB zXns0AmA9zsqpYT^s%Wc-dilkde71@1%nH4Z;t&%|G*@4iX9Zu0q%?D^ z(g`sMx(@vPAr%=`?Rh?w44|P95{o~Z7K?_D-erhff{4D^*@!tcW{%of zag~)6@B6f+Tbn1Us93B=aeYV!0in+1;|`XQHG$(upjDlnHvnFV;GXp3?#3mw%FPW% z?Pzaex86J(KN~xpqxYtlz=FcYTCuSVw+OeyqE~q*E2o`<@>BhpKvZH)TK28aB(t;_ zNlgP>PLb5^r{p4y3Yv8FC!@Iw{L%^qYL7>zuqn%F|NJNh?Kw#<)Gjebj*qsvjBT7g z7F_$3E@1?O-&#|_kRZ|22&cZ-N*@8arHDj1L89GKe#P8h9{&o~CoX&pFQO$B_t8yG zBK0v!3T^Vkip{jK<~8X0A;9BPc&rOC=7Oev2B;55ExC8lht+ukw*gAJ=HqEXpWH!lCv);V%6(U&vey6Q!B*JXzYGI|5{jF?Vm*B-|m3_XF8~0YH28JXYFXC zXl43Wv1(A-P{LG1dM5%;8KDtftKTOE5r;PyF_(Xz|Egr-gOKKp0ETUD9%LW_N`#m! zp|{|C1=4<3i)tw|YnZrd?g6~ji)z#IKvPD^u-QCbo?x9t@0Qi^&@{yw_x7^I{Q;r# zbxTY{d|2dBqPNiB4FM+@K!Qh|`-2a)hBB8Vm!KD}20xdg7eN-BcTh3#0j-jj0KJkv zoK|8Oj+S)p2%-!T~ni(w8y z2bBi(fQCck9-;^mbyf2f8W!0?SIs8`69Q|Tq*!08NBL32o0}^Y!G?+}b#vxKZFas~ z&(6_Yc#55#SlbwTtvZzcA~pRBBbO_8Mpb;AVueOUj8+|l(*OxUS-^&Y=#oT{!Za3T z%NY!+Npqnxxshg*@IjcMI<;A3XW`}2Z7F@NRs0>hQ$b^sE;^P(G6Qs*+4ZD0)nHtFg&N9-ayW33F)>RlP|q@YGPx4#g!%RSxD|OF>cz}^8^qTO_?7Ze8x+X8!xXcr~XY6&ls9`FmFK@U6+uWv6KYW z2xE***_AeP-@dS;pJS@dgo=7u8#EEA>F(3WRD6DZtmH$a?m%#3G?1%N_%6=_B>o|t z`*o4MCt1pbcs(-Qar7EcgeC1TdTsFeHe+nQRvCYsi0nvQl)}hX7_)~91k_ihl`j#u zp7PtNMro1K_}W%$%RW3*FssGui9HuAQHiL@b4j_I6#X_sK3&XdO9C^}%A82BsH82J zO8kukawef{%^_(Wp7MsXbCPC|rqc#|UGK}T1~)UzmyvMdxlH%%5jTpGr|ihDEZBRV zz6S_=pAHe-nyJunzl5!_Kj3Om;j(5WcoJ!bTc>diw1+6CJQ28@1F#Yhr)Y1!(yhHj zt{j?_H!FFb>GmziPw@}qBpzFOQz9sXrUKY$9}%B3J8t2BQvQPewvW}iFS(=)dmKz_ zkpxR$43E#P;Jkn(nEO6FW*to+d?EH4+(|rCG8`2=N@e9*Na)Z~sI0$*k7b^H2nWo) z%?D{bkCJOPztz9L{Bx62pgqLd(=D=R`B6S;zD&ST;FqOFqIgw|luj08m5PYp5Y1w@x31ApqPf6Ta3g3)VLETtQ&QI5D%^VAPDEiyT2#<`i^;}_0Z z>S#qT;KMqzY#)2ohBBWzRmNo=YbE3oofY6)Za39EG{ea0EEztp9O*2enZk5uq62XonH;NUj_wzu3ICcPqK}CqWPIkY z_rIMW{txAzMNDn@P3`m@EsYHA46XDH|DtsIPw&ng?`$xW-JY~Vt6X7eNuT&{c<>C@ z^X2CdVdsheCii@ooAeOlLT~N7`DZ?}$6A=K%sJfd<{jc)>iNk#HkHpLk&2o4D?NJ+ zl}&E_3f!1YnPYzzuHMgwIrF>jR+`(+s}tJ~lW%#ZM^Tlpr{KKQ+Z428(v;_w%lK>4 z{i~>CqjPSR&Ee&q_5_5=p1t)4soC_K60`mzAQ|X{zjk}YrfM|=-ANF{Swd>(7bq55 zG@8fsBHEBnX~-PFCn==3ZAJ~s>EPwDxu+jilF@~_IwEx*ooA1$1|L7*o=wH{U--|d zg{dx;IOr68AxeKN^MVhV=u9m}a+;PLovL$^>WvLXP9Vq6lrK_Ng{MxufV95+S?VU} zP{NRYra1V&6?zuE+V1cDB9&j{F+Tw_Dlnzk03dXc*J5b@ zel&}bC^jicKD_*aO*J)AHE`r5TFv|hmAX|$CGxZUOX1kgGzL>d1a>E_f(e7diEwDE zjKYljpz&Mo(jsV1XF`9A>WmbavzC_(`-_%C$4jlZ)8Ae05ZhphYRbyWA7dkj>E(?G zsOdCPYAmMv{e7aC#OLW?=>XPimmPrY-7dS9`%>7Y%e4o!=gt5jHI>_Sq&F_uV~#g2 zIDof1?5h^Sl|E1x(iJ(7HJA$0RiSs2`?60mEIa(QF6;8~axa|!K zv9#TdBDa(7Y}7{dYyRsuV&b=>A#iTc=9?dpFSPwzfmcYx*Iy=o42&=kYr}4*0a6hu zy{+}hytB5=fnJdUzIcK%dS~rDebGAjwG<5ufGz@lgpLJzO_X-SZ27XeKes7N+*bh+QZ&1A)kiDcIl*SXtg zQX)2&DCs%er5)w%dsWqmJAl(pHbzp5PlSB1bdNS9vewyK!L?V9wp=Y@T_!Q?_qJ%P z{nFfh?uQ)Zod?l1a;;yanvo&59F3m2{8R+LNL9(*_Vplb|`P>`w2Jah|^t z8Un~No5XfP)k+meZyV4Pdffq&j;+~jW{)`UaUqk#D0n$DQGrPX6#PHKRg~TZUySRbUb~rDs8`Xu4)l-zDoI-dO1-wzacf>sYWdq z!=^-)5sEH0L3(5)U4qsCnU)igH3ntCpTyY6KsMY~*-gWfESIFkfNfF7x-BC^tg!ZT zKZNZ&8e^>U1Z*IsSq_{cc7@4`ztg-q=In+M*yoZEe@HOFmLdyb2&>r^yZ=+`gbYV) zLLTt|!}%6tLg)cYy$e~62pMuxF1)i>e?OF=N{Vd1L|`fZ@%c)5Ly1`$E&-I^!zCba z<6-jv3ix3I4r=J4jNT!M>!N+HJ-g30cW(SE>c%m7S^@D?b&wM-AtkPw_z%;-leQSG zV>Gmnk9*?29cqgoLRF#qEA0b}aSv=y7jw-ut38bk#jJ>Cf@NMAN5go5fVS7h@CVz# z9JWJ>HU-qHvzf$HdGcS`h6?5*r;Frb4H}IW!sPw+Ae_={ey75<254%sjCgmN^YM3; zjs?HYTp&J}l+H5~9rCQ30%&HBF42A;^dpr-Ow z0?+;)9H2b8dI(wRbUmpZGY(X_G0K=gQ^D=g4aodp5?9 zr5ZEa9XQQT{(9+xpmHWiu}<=iGd%!|d(M9iEKRaGc|6k!fhjFiw@*M_WQkw6%lFqv-2Vu%egH&a$mF zbN1PhOM@03zovfUkFnGS?@C%^!8k3oKY-$@@TpfnfWlO4^XNHz&i3>n1(OC--VgRgSZDNL-isKTBsfr;bojtg||LG8YZ8+5?3v zcXXSK6SqfXm!@YVJE5lK)or8iX_leE&Zg!cvve?D$en7WYtr6;fzK|{3_cwEHorz) zqPP-$OBIzj)-*=<(VQOnRaa@h_4wzk%Vx^7TJRIHLWcRfc2)2n=5c!mT`LEB0tGu$ zV+$93LpxK0zb?9Zr2aIw=K~7U<3*I#;&4Xfbftl&wzY&ZUZyjc;XE zsfgV~6{~!}FLEjdUSl-Co(;=G$8=gy-I{kc^Dm0IFzOb&q_Y_KW&EGbXT}nwm0;=D z&nZR{1P-N!WU-LGiwx8T{ohTEFjjXA92hh2{6>ah7i9`X?~!a%28rAd_Q{oN637I0 zsYW9!H}%rdLEj8yi1bp5Xce!4-*ko~=%4q>zWH^9NqK3G?{h;~N-sZYO;5bV3Ktf4 zu!dByeXodbZt)Q{b&hZhugVF1bDcXXH9^~=s;hAR1Lw)|0h|zj zmRzuZTXG5ilTiH|vhXi-@z0gxzo?K4qQ$Lxd7uKliU|1u3dopL(T>KlcV9sKbpnv2 zwv8yjG6+s2dUslnfLx11fd~ltBUfX+#1px>JwH0VzO?U(<`hjUqme*?o6Vvu2IGgL z_BO6eHt$HdTR!Y+Ikg-2>ZmI27AAI}N*@{b62(<1x$_@YoG$LuQJ2GQ-n7nVif<`l zP9)em>ee%7xw`44buTvQgIzk`6)1;Md`yT?cLBI?Dk;Cup6C<1AR^*%>{`Gp1m|VL zgKe<%@DHEqP_87{D7Y=#wTWmlx}GeySO-5 zkJ9co>FbLsor5b>OhN#WIhsmgYF6uWHi&!JgS#Dtz$`14u@ovMo_jzz7&NqqzdU0Pxy~ z_gjB%kVZgZr~hF<|M#fbe{+fM|Hd%oEes89{xTvxVt=BTt5$025jCNkoW{J=zD08Q z@M`1)V40)K6qaVKF=vBOzqh|X%I>C;gaT&9#wXh!jBx6xtE8vCNL62UJJM_~?_9As zvEHLSny{sOQ>NwjiYK=eNx>Wy8`%!*vB3(e`>o57KG3uT)b}GkK4qU+_J!P)AaJul zt`^NOzhNF~^Ra`HP7V@21y3=Iel0wI(X^5%)3MoZhNz>b+OZ* z7fYsv&g87jmTn;RpGL2sFg-JM)z!L>8lNeN9Mxt54q#IMn~d@zOy*w?*{)&=>`K`h!x*Pu_BV; z3FI9^6@$uq;!9yqJQN$F(IkeG(kUl%gQK8_l5>b^YSFbXvQqde&33m>(=ZA*kpw}F zK6sOaKr%1$&f$0SK!JoB_8Y1LwgBHxSfJ@d&G%>9&t~j1t^=SduqsMPS%oL4@cGDB z{!_UX2h6}JOwTZ~jBR)Gtz~F*NCWE!y$#dkYO*5IYBAz$RX(vnRv1(XjZ&05D8XFk2CjjW9x3_cj`IjwT ze*d%aby$KfKChUWNSpnab>n{LP5g#AM?vnK%jjA;1p;vmwP9A|#ztQ0YJnKKfkx8S z5UfU;5aQ%Pc?mc?OeG-bJt331W^5)d!53YVp?ItuVl0rzk!U^a4qG`_RDSFNaRX#w zf>O1p^pm570Az1BG{_+u^j+b9VfHZ1(AHL;SylFLCn#e78IP6m7(2V+(ZS@Q-9n(_9JX?2 zf#vdsC6q$sEpBq6ewk?6dR>m}5aGHY8$7fa+`P&Ld`k%z%-zpp-Q;Sc(Hv$=5JAFe71s5Ab0=mBlMn$Z; zcpnYY(8o-RmHp)L`T;W1Jq@;sth-$>i?tuwuD}(odY{sSoMo%emRQy|5N>Z=F%C=Q z{;@C&5m%~@-(#k+he<)iU(A!~X-suSyX$?>f$K=2@KSo=5KIkduqg1MB5DT`&RZfa zTUR*_w8V!a1k3iFBq!Qhi&hWZwAPZ0gl&$UEiUYT|6p()=73*K@YqSX$2x|z_RG1V zybJ%S-ADzxW?08@@h1$DeR{5n^viip z(l=QC5O+D=!(I3YQBX5neht|tK_I8%=Bsk+ZF6ax4sLAlMeFF-ftnuw0o@8$dq9dK zfFPc*yv@1mY(OtDjZs8R$7&!ea3a~k6V8+2-aqZKk282q?BerH^(J@1j-jXzA+Sb; zuKtEybaZxEEE}$o{vG0vqi|3q!|L&wbvQpgdjEQi_K#Tc|L0%wFT+0dM>jIq1#bcZ z5A>1k47;xfI~WG17mp?qOvD$y)R4hbQ(w4t(&lmn^vO=EM}u|+zNh;=A>qkR--erI zefdSo)@ycj#)f(Bqa@Xk{jZB>OWqda${+vaz3=zJc|=uzk( zSyTSn?2%p@XO(lS{v&s63Z9+JC~5jpnq3sg-;O)_zS6yb(VWk)#*)wWC;}Ln7#`9N zjVPvXDU&)kTPuP10WA^&>mJ-#99pnh#S#4`Z1Gya|70rz!7O!efX2&8dj zmck;gcE$Vg=<2AL{)FEa%LpsMwlMFG50XyaDiUn+D2?mIcsuycxsfzLD}84C%=zAZ z>$1(uO$z7z?d_X45sN3*75psVjaxTc-lZQ*+JT4TBUpjVp{vr-`a7SZqIny`(}SXDftji*}&ds(aKF? zIaTSWtpL>~arQMv%%SXD_n`)bD=kT8iARA`M9pJsqm>A-5!U0;Ej1`u1&`lxGN*5V z%!y$-ol>yR0amM`D3E&FM=Vw0I(5iq#SMQwa+p?cJN^bg=%X`~_Nee){wY)2H=YWU zM)XL6-BRB(dCu%GHgYzD{T8gRhFJgtR6(D{XweiUq?OGYGYUQU`&p-xXU>($@^IXN zBZ~`js(IDaY$%JdUdnjU5S-rJ0TuKG`lWi)dPWn_>7&wW!`v65A=dQluonL;i0`wh z(IbWaBGE#6?sz-DLgi6RQvRN_0w+-+fQvy_Yijij;!aAQR4{w+k)Fjf_fZVKfq4kg zI*_UagC^xRx+4U-WCe=^8d1*A%L?LF2SqV8W2^onJ9eo8mQF6h`a5+st6W&Y_I*G_AT)O6t5lOrNy zp>eb=ckC^sNgR6VMP_Pii%MyRugm@VOkj~v9AIPo(cfKEaC|i0+*_-&X8pWnwh2*4 z1cqeu`mOr71500I63S%SmEqb$1V?Ns+7!3WEoI5#qiX7U){@sRAj#LnFY<1QzK)#4 z+ER2Wp5s5jy!I8B-?tc*1{u#&lkT4U>M%LO;G21-^|$VGhThkntNGrM&+d{7Y|Y=p z-VVJfh`iZH)*c1w?oG5$r55kD8~{6$!@-kv14m3Eu8&4U)z>2oNLXFVmeQkbCsRP4 zDrxudPbT;v4HORU)+cdC_R?id@zNW>zZu(Cq)~(~-zRGZ&D+XxC!3eG3cxfDA#dda z*+di!Ko$hSMdqu^$wEl={Hm$s@7iy%j@o~%N4D!}tcp}goLe)sBQK=qEaQsKbqR*4 zkQyABgXaW{`K>ih6FMuW{9Tv}T3>}$!(yFg5m7;O`uA6=F5efz^(xfn$cDbzVrK!A zp$J|4B9O<}gvZdiE9lI=CNSBWuX`#Lub~wh>Lq|L%z&WCyr?6wK`^_?9k^drSo|#G zkA~!>GP%#hH5fQe_XW3g;vBtfzJLalxbh;P6ju?HXST(Wn+V-~b$n?tL>o;B8z!RS z5%uO`mn0w8mV^9*e#0ikoKqDJS41_s`q_Q5BD%d|NutnP+Hx)oNVjH@lBSwrxm7cb zTjJu706|$bddiMpJM-`GkZvvLeW^3Vd#yhF5bJwQx4@?kxaL=g=8s>_0m0uq2SqDE zJ#Ecpfvk^cF%(UDE5IPjTUxGR#b~n*c2y`sT5)A)PCU8Ag;(kn>7|N5_nDW9QFFu3 zlr{J{7)dz-3s_Ek-QNGiL=cVKj6^?0XxhO4F6I8C?X~~1+~u=2a8WR{v@x{Pb#S!% zYcz1|GyfuS=JAY)!(H{LEm`J@D}#v9>c;~Bj~mxjC|(V=6`jUfSzN}Z1Wh!jTwg1ZoJOa`7`-!YqhY%BL71mv zH5TR0=cD1B#^?%~h`vc+O%m-^M*WuHl1_w!37UhU;%I4XHbwE^TBsu3&O-MsLSR&R zkS$GzV&NV#Mf*cSF=dS`&W<|-cnj%0<7s9`UUb6x6#n*o)8U!Fd52rXQ&B-)K;5zn zDV2hGTt3I_VikcG9s^8lSrGNbq}W;T?LjJVjI}+_hB8Rh=po*cR9gVZwWJ&XQB1Zq zUVF#xR7{u z-Dvtg)89e@`Fxvl+Im+vRM$6hwcZfhU9NDi_#^~A$+#EVzeGLi7%;pvbYL$#tT)}Z z+^wAsP9LJOfX4M{{Z7%7(Z|h;O^VHu7ir=;*~6Ldf~dEQdSr+39L=5`eLL6$4Q7w# za(ae$AgIXfS%XNS4rqepV3=uoqbZad(E(4Hc47PbcZHWJIaj@n>+}KjU$<(;r_EAc z8g24C#07da!ukv$n(`4T6%)~)$4;gDb7(YqEz@MThdT%d9=Gm%sUO~ zX?BlZ;+%yTm_p!*+6lgM%FX0ZdPJ5^jC`OMR*y@ zg?Y+1ui@D!$QIe@^ieZ;xR`v;*&EA6M4^14ol%8)F5H4iy)Vr>X=TEnvDiTUp3&Jt z4_RNX2|mI@{+@_s3bUV1IZZl_@{-kAR(QTgmcoV!}Z zs+`Gu?ic*ERjcCr3GS>A>7{}Z(|rW$_x?#v>j<*c=XdGlt)O-NQMtkVjtbVwx zUNC0ch#SFD{&G8p`@ZIo92M$5%5d4n@_PJhV34n(lGh%7ncpJ-C3(_}1j9up{TEyM z1<0_}a-||7#Q9PtDCiQO;L9WN9|s(De<{NB^KjGuZLj`6L~i+Q%>U|)%l-#W_{X+m zgefK9KeioL5o+{uc&`)|4MgXNFh{IjSztZA|M#}z_}Tax^KYYr{kvoIZge6P3rKy$ zAR;V8d_>`IfnDu*XpaHWq2P{VwV_KZLD)SesiVceLbdIQ%3=>c5nT3UmoOR?>5ggj zAm=gk9+qti`Lrp#*^OFZq3JFp-z3k$KM-8idDB$dnCS#E`4|njTO~n7A@@w9)^h?f zJLOspB|KYZNlE6?gmg1+&oA*I%@niAqX6M8?g0tfXMW(Gr!7(9iS^%Ja9)}TGr>(= zgPko~HzJN;t4H2Cu#{MR{XU-R&q5VMn;$4~gD~}u_11knO>+wzz?|nn(EqXUXar#} zgz|ag{5^F0|Fdal_=^}-#IAfIC`f@HGpqFtiw%xL0b0%8@bhITvZqSLc#U9MkrO(3 zb0L%Yd|P5w-#~D?gAk+_zgd z4HxEP@p2~)0b4Rc!488uP9u|eU^kS;0x}$7c<_+>$=`W9U=E{%j8W-yfL_t=9K#Ug z{E(5bgj%{^ z9xIFkEL)5TxMAWVLs4Z>beFJZ_*PQ5g$1My~ry;?5Oz|1_5r`*ZJ^Zo~`w+G&toh*xqpF?NkzEKZJtV9$^Y z3IRDtRB6vGA7U#YlWY_;zF5)AuE+?lp8(3+QqOtRR^7nSKt-&B@^i=KrKdB;1%S>`Fo6* za9kvm{CAx=Xy{!5{@fF&t;Z%)4A9Fa~^{EkJ^`(ZvY~2`fjQ1<&TIR%wsXBmbukK%Fb&5$cEw~8E0}qa2%&D5jMs~ zHDy=1@~s9+O~$PiLmp}_EyZ7G@46E{4V17g;;WoJgi9wAMCp9F%*D8sGO&pWQnCD( zjbj`(6^yf~j&VnM^8E2l!$rm-azpWUbqOIjK|ks5AyAtJ)#gY`q^*(ej;2EO$`VFg zWMGZU)l1Oh{n})2DJ=P3@;D0h@Gm|{jri|bOeI1K#DiOpm-75}A_B)FkZY>f_m%od zJvtbyX6Y(NC*vw5loDj(0zMb1-@{9#)rmW`(xZ1|REh{DgqO{y4jD2tY;F%ts_4Bh z=BynG{qOq7CQD(D)~rXJ$vm7yQOB;5h0DHXBdqbXb1GTq6I@IyvvkvEG3gpFl5${I zTivBu_E^GtLtynEu%-@Dwmi_|dLOLXauCQ#C%`x)sWR|6Tg$!tu$8&Somh~zl$PU7 z_`RSk!;-?dNUg@Q-dA<%Dee~P-5qAz`UgQlXtOKABBEriq}9cWW+%{l_R6T$M8{#G`<|%0`q|3L3d5R8&^NfRO%d`ULXH)MPiUKRY7zW&ZS(M#`Vf8* zt73cQ5S%iXDc=BOD2H*?w*=-!^Yr{0h|!QdbmFc2$BFMt*QzW7KUs42VQn1A>T<7s z;t+8j2-_k7@W4nkd6z0ICB2AO3r@2f<@`Ebw3B;L91m)0w?^n=Fe17LaFcXrfRAabOJTToRB?^eOq~CsV(b zqZaP!jA}4bdpWS(fw4~dh!txvHg{?T`L+*kK*)tMcV8;5)czYvli%4SA275-9-Hp0 zQ|uSNZx_)m@q4Z?OSg)jvake1s3;!TddPyAx5UI(a#WH+nVI5g4Y^NtJBFe(}BB-@*hE`8o5UF4_ai=pu&vrBt z$IpM31S}K2o+_Mh-Qek@Ns?0s&8WoG;d(M+s4?fKqwU zK372I5FQ~ckye|IB@ao1;R@uZlASEEw*VIEl4sUwr#d@U?-*s;H(50OT}C|gCK6S- zCG4t21neXq@bXQ(>e(bwG;{cApGp`nx5e~umT&jrvJX>k38~aKXxouqxDmchcqa(P zjsVZ@fWniisqlUk`O^QnfhoLE@uSvzQ{qn042vfi`ANBsoVPh6q%B#H3liwMkle5! z!ciSJyYQuELB0ME#4Jb@PV-()%hYFT{gf7SPSlz&&6Mn8O9h<41jMO50>^8%v&Ir# z2f#+E?%H?99dk~=+^7bVH>YJyHI3I_h9xS$+yqY5foUzoZURw(d1o1$J?%C=G2j09#+E)--zF`Z zg&e0_YPp&`Ap|sMqa+|S zJWd@EP9Okc?{(W6_Zmr{0r|-2Q(Vw|0rhETpniP(SI97Eh%ji#ut1OGByO*{ueWaV7h&21;5nfbNJb{OYLaRykGX##XWFJ;U z(YlX}KpQ3g=v%8jUd>@P z#%Dq{1H(2WIjudCo3Z7lZ;8Wi2AEu`RvNb;fV31O{tAfQ3YWR09ryRD&DxS;_SWcna}u|&;*w$F1+5REU=@Xy=A`u7_JClFVhkw5&%WWIjMUx! z81!Nd{G^dkR)iUm<#P0L5!|>`rs;iV!iKzjXGwS?5Sm*QGlwzIWnq};>*$Tqg$Nj~p`g-#qh!k0Jc zJif3Qm@YvKiAG1(Tb>n^BGkees!DYKg8f-2vlLM_!HyNh!;8M5$^s_I%@+dk(?Hys z4z<>&G~co_j3PrUVJLIid^9w)q7m6S8#R0%i!QuPh%g0Cj- z*@zHa;tW8k`c~SOH19;mV>a*f$%sb5{=MUDOK6$LcxHsHQU>?W-!?$OVj5hwuilF` zoPVqR{dZ-fe`-q0Qj+>2wj+PodSx`$@k5H-DwKuIt6B&P&vK5d8<(Pj*%*MirI7Ml zr|T!|AIP=3o-M;107j#xPq+0BC3z&c*m+s)(M}$}%=&Iu2OF_+df%k;kw?t2gV{;< zTM3M`MHPl!FcRmx+$Jn-7#hf?y3<5zzX)#1K#F1jio;@*dht>AGW?i6;^_lt{Am&g zm^9+zIH`S7QS|(_cS_)yEx1Kz+v;_D&2%zN-Hq+VpsJ3TGOO(E#b$uPi&pzha5H?m zlX3NGvEKL$1K_xPW-w-!TEK*5hvnCl;_PkDZ3$o=CsFcxe%eHg4ITXv{kg%8ft`+BV{Flzx}rP68| zw>X8Z=~xdlvdy49<9u0pX5wNLxPAtZoU-VEG1>;)d0KXxGouZ;w%G2E3R|=rVM7V! z3(eGU0VobUwA6J?7F@LpsJRIV?bL@V@7u+9cbJ%v`{g&-Xl#aT*QcSqwjH$k>F{LV zcTEOXe~;;hHJ%7_amtNE~TR@LN!v$0)f4j0vbDrpwq32Nx$91mF4+EJ&2Mip_ z#E*zSp~1#cmU`oL1^?`f?S?}(Mkgu@0PX~u#uMC&joVjw5E@vsjeH7H0y7?l*hd%E1P|RPDb9Of! z7ZVpVclxJiBq>1D+dnt*km3-Oj7?~1umscuy~p_90|H2MgWw@Ugn-O@t?Idgf~RcT!FSi* z<~5s!R4c2JJs-XS$f3g?OHn#&aCN(^i&U)OY^;hXf4^-{%DS>OfJ|Pee0IKUKXt$P zT+7@I*1h3=nz;sb^{0csXVAIl9?ao5P@D3CMhB< zA0&o=grZOsCJ|ApI%q5+Jr;a>;6?zB))r^iO9H?Yf)k<>@-qMy%E6L--fGOx=o`sp z3c`5B;2nZD2xA;X-v^{HX%F34gv`);C+|4{a?GBA29^M9t?`j`4BFSET~_F{`mL)l zXjEJ`B_$1zL1|{zx4<&E^1ZB(~ zY{(WRuc#nOudE8hrD8C!TSb5Pca=l^SW4t~Z4{AK1r!nh%4&JU96;AvNd#uW(gAJY z4}j0a-EU7IBoqTzVF=(T%Dl%_o*JB?>_F*T{G~6&QJN-VfG~T3$u;hF0CtR8llE9u zCXd!UT6KEQsw@&$Y3qdc4gJHn(`9K6V}pP{MD^H_7li7DL5H21P@^v9O#K=6*K%#< zF#|PATO2fIz_!q^1g|1Qfbw(+Q;5fI)$vzXiMElw@{ z9qqKr+nen>$x5ZD)|JkF_0RL@ieK+9hELZrdzP<62(quYYDYdl z=r>fYpfqP1?~-?fFIO_*a5D{YDF;_AJ93F4(vO6ENC>BM>|)jxwV36(8RsdPEUnJL zDB?>)p2d!Ym0S5tVK|7N$jtmcG(b;0%i#p<=<_JU4OpO#+M{Y3;Ka`!yOn&@`f_-up|~(tFMe_E1ga0N;+D^xWU{X zdBS|Wqw3MaLk%879yX-{oAgBl->cDT2~0xw`({BNN>gJaz%P=6umtCew4+8<*)14q z0$&hOHi0w|Avi-iks>G#)`Op0farnOJ!TPg#Vib)3R-k-5)vbyc{jFc}T zhbcrQmP>ya9e7%_=SbUOKM3LqMU%CK?9XCAoJk${@OT=NSe59c6~RjWN*68D2_%=~ zfJ!m{Bb=5fTgM@6lT6WtdiZEWsrlaT_pl^8FCg`K{bmtZ+Ht4y`{6qKGe$7 zixjNGFx3)&m=@Jmq1r5$A%s!#`(T$6Ng3fFHp8Qx9x#R{GQ(OzQE%-e^XhrJA{O!- z(2g2EV;{CG!&Mff2%3rf;}PIrY)u7pXiZ!Qs+8jT@TRIj3IfGWs)>)xBi5swyE@`~ z`YYaEj}g9K&bSBnQ3Qym9fLhW12*39C!nXBVs}Ap2zXF+H^oOWBnkEInlqxeJ$G4( zYV)F%Ej@9(-QKutd4#&lx$k^QVi}_UWo}u@==EJ(n8*r>xX--1QMY>xObSc8J5}cA zxlp%bfpj&aWd!n^mKD9U1_iU&p*dQ`#EF}tXfetHeF3Sup(2x0y64*P&++07Jx&4& zZn*@bkW69+`DM`c91}FY-g&`nhY!rwarcRESVa?%UFTCpxCo?WF-U!K4Ptr)EeRQJ*9P;O6LGtDlyOKo; zEC)9Q&RL0ZI&ncuQGCK&I;XfMO2qKn+W>;2i3TGzbj$ODFD^vXv}*{-ulPUK2*DAa z@Js5Nu~O@1K2q{Uc84~}EpwF+Xe^<+Oy#mr5F`n+rD|x=l~9Ea^*|P>sGZyr^eLDM zyL0pI^yUx}p1)$`)gYcQLR#Ap6PQ2|wo1TR&yGbIcBnxSy68H?oe-y}`!ZKwzb%>i zQfDG3b&PH9-nB*j{?T_Vj$cf60)4bGZeQQU-Yh)=c>PKDog%*^kT3qbXE#Ys(R&{C z&RIvQyD!~HPtnek()JIb9B;3BS~2g*CRQ(@TTTnH%!)Cqcw357tXxiDI00~TK~Y!t;6X9rH-ubG2PB2j%g zqX{eKm4e>VlMfa-bUa*!FU|~OM=slc*jpNC{g0}v*a|YiCdTPKFKFzU{#3{Al#GF9~vj5Fuv#vX)!;Vi2JiI)bd%WleVQ3mq>IEt*{#8b68dBm%69pHuf}* z>hv8kNf_b>SK!zQ*?s1uo|+fkFo``U-^fBDZj#G1h$-B!RXiY=U{| zlzB@vW9RTB#FdMPtlQ2^DOI?mH_lvj(|ui}7u*RKaY@=@@punkx>6SMwGPD`N-`U4 zr`TI?K9#6pEQM7y#RN&g&)!CX2AHL%jfim+B^d?V_fbkwA>}lR^5%|vdPiv2Ir~0Z zDHJDrN~f#by?&gpAIMC^zMhCzPL2#JyL50` zs3b8#GXJdJ|8sGv>xgb6oaPQORg9w4Y|ze;`ARR@VgQ&Si!|jscA?`4+I;<f%quF7D^+G+F7n+J3Z?4Mr-Qtoa9!&p0?Jw88tb8K;LbuKLTK(kmo}Lbw?vDwtCdN6ffQWc13pxH2}bkPfCg~L&SRjj zob>?Z|c}7<$0r%i-i9; z&CXbL8KYS=yFO>jD{BBALZ_?aXk-PgEH;ICkOxV&~oV$Tf(nN*_qKHJb!BSeWP4=Or;nj*yWq9rs zAG--3m-i5quBV8DUrv_rMB^7}5K~=JHhq#@oZA=LbSZxuNdTS93P+I4cfSI&&seCN zD4(jIpTnyWiD?HKI?(7aRSy6hc#b2m6e}| zEd-2!!Z;Vd=GCukG;257|k?EgEVYp7$#>h@}4iLNDl+7a=l%^Di2`DKdark-H zWgxbNvOe-?T+trKkA4=7IkwAAvFi-cmr#8+j3T=jukuZu$Y2wBs)Otn06a^k}l7_kPpe(t!1ww)g?{INMTsbX9RcsjM`G_|@PCeAtaoNYYFwyW0`ZyVOyc^W9v0y631|uURS)z>e8R3bnNe~sHPj(ui(;)L^^E&cC z$Z9VJ0}&#JfQeAGO=_Y|!fnlm65zu%P%?HlB~ew>On

m&LA&x=@@?sC0+fg)0py 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 P-#GXh$GbfcZ0PwPAPAw?O%+J$H z&PgmT4h`XCVD2i^2vT{nBDl1In}Lz#1<*h+F*V3H|FVNXZTIDtmMPPwOxtF%ly#Mv z=a zRc_vGUtjy5@qs|KYD{8<30JM@v${EK$y3?$M5@+F?S3E9eMq6>@=KE*p@&)WZb`E$ z5(AtX6QAnVpTEkv+%}!HZFb+aw_naAZcpc5>$v~Eaqil~2TIzGmPs7p6XxNqA)9xDe?D1G;{$GUsuixowZM#fOwoYfhdq$GW z`LB}O?(!lhzG5NYz>ixsxRifC4lxj!U)eZa#(0ga#PiFMo=;dGEAM@1AaZM=wpPG~ zFjwB~Cmyq&X$xL<(rnd^My11(9rk^icZe8gqIHJziZ_}CH-TI_g+*n3x z`raPpwn_SrX7n4stiNM0<>=3jr5imy`B-LLSQ@g%px5nDtGex#|JNTc3^}UjtJ=S| zgI6a&#k5jy+}ZpLhpY>j{#pS*7?UkeA-cHQ2xY4ZESVy`~U4BXahy+muX>?@VuZyvo{ z)9+Ou8J$sg@YizVk4f7q=5U4;AM%;-Fz(o{|I&!$)z8Q2yz|^g^;OtoSdrndt^_E%ohcVbxhVVbT<@sgo3r zW{SO0`!->h=bNmY@9{s{^E^~1L~LN`?033T7$lI|tb2a$Z;Rr0wvXlQ_t&s~VDc|r zBJpi*LRxE$*s*^%9_`a+KEKteJn7~Zp4Rtln`+hOboQAz|C|$FlznRDm$xrsECikP zdQ!ZFCM>-4bMiLjmp>ne^QfLG-zmTUxu1x)B4^6k4;d#%NH}Ty>n3? z)5JR^f-Pa%-r>4S`#O0xac#^>@_l+HZ*zIoog99@-}%2*Jz2*Y ztLi0tUVLwbnNZOI_uz%Qcx3Eki(YEhOP-Nala;)%A$3ukO{wVIN=4_zpXW2JjNE%6 zPDHNL`@G=EhWAA`5-&{WWbXgya{OIOmM$5v*hQ|B7wtvNiRE+{VlV%N7XFrJHU5x@kRLwO$$?&%w4!eX|=H0SN}QVWooebPia zz&uiAeVgtZp~G^8ceX_NN0?4tD;%rLDffp7H4(kL{EvePn21V&Serm1^2{qqElNvF zPK6|)+JKFIhYbX5r(bYrWD-@@4%(D(U@g0eSaes{zOS#Q!`@#wO|4+cA~ zLPw@6to+|g&cA!pW{bfd_UtMJ0CV%&t6~_HEOi8w=lu z9RA`uP5=Kj4b6yf-<#1dcP}VR%)aJR7PkN0Q)k9ILcgYeQ2my8qViUH5Br&(2OPho zWe=Y;?|V3<{-)t^#yRzj0p5&EBEY%=R}BITPb7e=J^`5`#|$wAv)Yh=o3N!Z5Xi=t z%kfnrtdL3stpY*z0k$#)ZaKpSCj53|lrZQ + package="nl.sense.rninputkit"> + + + + + + + + + + + > + + + + + + + + + + + + + + + + + + diff --git a/android/src/main/java/nl/sense/rninputkit/helper/BloodPressureConverter.java b/android/src/main/java/nl/sense/rninputkit/helper/BloodPressureConverter.java index cb25870..01a9e3d 100644 --- a/android/src/main/java/nl/sense/rninputkit/helper/BloodPressureConverter.java +++ b/android/src/main/java/nl/sense/rninputkit/helper/BloodPressureConverter.java @@ -8,7 +8,7 @@ import java.util.List; -import nl.sense_os.input_kit.entity.BloodPressure; // TODO IMPORTS +import nl.sense.rninputkit.inputkit.entity.BloodPressure; // TODO IMPORTS /** * Created by xedi on 10/16/17. diff --git a/android/src/main/java/nl/sense/rninputkit/helper/DataConverter.java b/android/src/main/java/nl/sense/rninputkit/helper/DataConverter.java index a5a9476..906b2e5 100644 --- a/android/src/main/java/nl/sense/rninputkit/helper/DataConverter.java +++ b/android/src/main/java/nl/sense/rninputkit/helper/DataConverter.java @@ -5,7 +5,7 @@ import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.WritableMap; -import nl.sense_os.input_kit.entity.DateContent; // TODO IMPORTS +import nl.sense.rninputkit.inputkit.entity.DateContent; // TODO IMPORTS /** * Created by xedi on 10/13/17. diff --git a/android/src/main/java/nl/sense/rninputkit/helper/ValueConverter.java b/android/src/main/java/nl/sense/rninputkit/helper/ValueConverter.java index 826def8..0119197 100644 --- a/android/src/main/java/nl/sense/rninputkit/helper/ValueConverter.java +++ b/android/src/main/java/nl/sense/rninputkit/helper/ValueConverter.java @@ -11,8 +11,8 @@ import java.util.List; -import nl.sense_os.input_kit.entity.DateContent; // TODO IMPORTS -import nl.sense_os.input_kit.entity.IKValue; // TODO IMPORTS +import nl.sense.rninputkit.inputkit.entity.DateContent; // TODO IMPORTS +import nl.sense.rninputkit.inputkit.entity.IKValue; // TODO IMPORTS /** * Created by panjiyudasetya on 10/23/17. diff --git a/android/src/main/java/nl/sense/rninputkit/helper/WeightConverter.java b/android/src/main/java/nl/sense/rninputkit/helper/WeightConverter.java index 9b3d17c..0d0dc61 100644 --- a/android/src/main/java/nl/sense/rninputkit/helper/WeightConverter.java +++ b/android/src/main/java/nl/sense/rninputkit/helper/WeightConverter.java @@ -8,7 +8,7 @@ import java.util.List; -import nl.sense_os.input_kit.entity.Weight; // TODO IMPORTS +import nl.sense.rninputkit.inputkit.entity.Weight; // TODO IMPORTS /** * Created by xedi on 10/13/17. diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/HealthProvider.java b/android/src/main/java/nl/sense/rninputkit/inputkit/HealthProvider.java new file mode 100644 index 0000000..ae1aac4 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/HealthProvider.java @@ -0,0 +1,342 @@ +package nl.sense.rninputkit.inputkit; + +import android.app.Activity; +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import android.util.Pair; + +import java.lang.ref.WeakReference; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import nl.sense.rninputkit.inputkit.InputKit.Callback; +import nl.sense.rninputkit.inputkit.InputKit.Result; +import nl.sense.rninputkit.inputkit.constant.IKStatus; +import nl.sense.rninputkit.inputkit.constant.Interval; +import nl.sense.rninputkit.inputkit.constant.SampleType.SampleName; +import nl.sense.rninputkit.inputkit.entity.BloodPressure; +import nl.sense.rninputkit.inputkit.entity.SensorDataPoint; +import nl.sense.rninputkit.inputkit.entity.StepContent; +import nl.sense.rninputkit.inputkit.entity.IKValue; +import nl.sense.rninputkit.inputkit.entity.Weight; +import nl.sense.rninputkit.inputkit.status.IKResultInfo; + +import static nl.sense.rninputkit.inputkit.constant.IKStatus.Code.IK_NOT_AVAILABLE; +import static nl.sense.rninputkit.inputkit.constant.IKStatus.Code.IK_NOT_CONNECTED; + +/** + * This is a Health contract provider which should be implemented on each Health class variants. + * Eg : Google Fit, Samsung Health, etc. + *

+ * Make it as an abstract class in case needed to share variable between Health provider + *

+ * Created by panjiyudasetya on 10/13/17. + */ + +public abstract class HealthProvider { + protected static final IKResultInfo UNREACHABLE_CONTEXT = new IKResultInfo( + IK_NOT_AVAILABLE, + IKStatus.INPUT_KIT_UNREACHABLE_CONTEXT); + protected static final IKResultInfo INPUT_KIT_NOT_CONNECTED = new IKResultInfo( + IK_NOT_CONNECTED, + String.format( + "%s! Make sure to ask request permission before using Input Kit API!", + IKStatus.INPUT_KIT_NOT_CONNECTED + )); + protected IReleasableHostProvider mReleasableHost; + private WeakReference mWeakContext; + private WeakReference mWeakHostActivity; + + public HealthProvider(@NonNull Context context) { + this.mWeakContext = new WeakReference<>(context.getApplicationContext()); + } + + public HealthProvider(@NonNull Context context, @NonNull IReleasableHostProvider releasableHost) { + this(context); + mReleasableHost = releasableHost; + } + + @SuppressWarnings("SpellCheckingInspection") + public enum ProviderType { + GOOGLE_FIT, SAMSUNG_HEALTH, GARMIN_SDK + } + + /** + * Since our health provider bound to weak reference of current application context + * as well as the activity, then we might need to re-initiate instance class wrapper + */ + protected interface IReleasableHostProvider { + /** + * Release wrapper health provider reference + */ + void release(); + } + + /** + * Sensor tracking listener + * + * @param Expected data type result + */ + @SuppressWarnings("SpellCheckingInspection") + public interface SensorListener { + void onSubscribe(@NonNull IKResultInfo info); + + void onReceive(@NonNull T data); + + void onUnsubscribe(@NonNull IKResultInfo info); + } + + /** + * Get available context. + * + * @return {@link Context} current application context. + * Null will be returned whenever context has no longer available inside + * of {@link HealthProvider#mWeakContext} + */ + @Nullable + public Context getContext() { + return mWeakContext.get(); + } + + /** + * Set current host activity. + * Typically it will be used to show an alert dialog since it bound to the Activity + * + * @param activity Current Host activity + */ + public void setHostActivity(@Nullable Activity activity) { + mWeakHostActivity = new WeakReference<>(activity); + } + + /** + * Get available host activity. + * + * @return {@link Activity} current host activity. + * Null will be returned whenever host activity has no longer available inside + * of {@link HealthProvider#mWeakHostActivity} + */ + @Nullable + public Activity getHostActivity() { + return mWeakHostActivity == null ? null : mWeakHostActivity.get(); + } + + /** + * Handler function when application context no longer available + */ + protected void onUnreachableContext() { + if (mReleasableHost != null) mReleasableHost.release(); + } + + /** + * Handler function when application context no longer available + * + * @param callback {@link Callback} listener + */ + protected void onUnreachableContext(@NonNull Callback callback) { + callback.onNotAvailable(UNREACHABLE_CONTEXT); + if (mReleasableHost != null) mReleasableHost.release(); + } + + /** + * Handler function when application context no longer available + * + * @param callback {@link Result} listener + */ + protected void onUnreachableContext(@NonNull Result callback) { + callback.onError(UNREACHABLE_CONTEXT); + if (mReleasableHost != null) mReleasableHost.release(); + } + + /** + * Call {@link Result#onError(IKResultInfo)} whenever Health provider is not connected. + * + * @param callback {@link Result} callback which to be handled + * @return True if available, False otherwise + */ + protected boolean isAvailable(@NonNull Result callback) { + if (!isAvailable()) { + callback.onError(INPUT_KIT_NOT_CONNECTED); + return false; + } + return true; + } + + /** + * Check Health provider availability. + * + * @return True if health provider is available, False otherwise. + */ + public abstract boolean isAvailable(); + + /** + * Check permission status of specific sensor in Health provider. + * @param permissionTypes permission types of sensor that needs to be check + * @return True if health provider is available, False otherwise. + */ + public abstract boolean isPermissionsAuthorised(String[] permissionTypes); + + /** + * Authorize Health provider connection. + * + * @param callback {@link Callback} event listener + * @param permissionType permission type. in case specific handler required when asking input kit + * type. + */ + public abstract void authorize(@NonNull Callback callback, String... permissionType); + + /** + * Disconnect from Health provider + * + * @param callback {@link Result} event listener + */ + public abstract void disconnect(@NonNull Result callback); + + /** + * Get total distance of walk on specific time range. + * + * @param startTime epoch for the start date + * @param endTime epoch for the end date + * @param limit historical data limitation + * set to null if you want to calculate all available distance within specific range + * @param callback {@link Result} containing number of total distance + */ + public abstract void getDistance(long startTime, + long endTime, + int limit, + @NonNull Result callback); + + /** + * Get sample distance within specific time range. + * + * @param startTime epoch for the start date + * @param endTime epoch for the end date + * @param limit historical data limitation + * set to null if you want to calculate all available distance within specific range + * @param callback {@link Result} containing number of total distance + */ + public abstract void getDistanceSamples(long startTime, + long endTime, + int limit, + @NonNull Result>> callback); + + /** + * Get total Today steps count. + * + * @param callback {@link Result} containing number of total steps count + */ + public abstract void getStepCount(@NonNull Result callback); + + /** + * Get total steps count of specific range + * + * @param startTime epoch for the start date + * @param endTime epoch for the end date + * @param callback {@link Result} containing number of total steps count + */ + public abstract void getStepCount(long startTime, + long endTime, + int limit, + @NonNull Result callback); + + /** + * Return data distribution of step count value through out a specific range. + * + * @param startTime epoch for the start date of the range where the distribution should be calculated from. + * @param endTime epoch for the end date of the range where the distribution should be calculated from. + * @param interval Interval + * @param limit historical data limitation + * set to null if you want to calculate all available distance within specific range + * @param callback {@link Result} Steps content set if available. + **/ + public abstract void getStepCountDistribution(long startTime, + long endTime, + @NonNull @Interval.IntervalName String interval, + int limit, + @NonNull Result callback); + + /** + * Returns data contains sleep analysis data of a specific range. Sorted recent data first. + * + * @param startTime epoch for the start date of the range + * @param endTime epoch for the end date of the range + * @param callback {@link Result} containing a set of sleep analysis samples + */ + // TODO: Define data type of sleep analysis samples Input Kit result + public abstract void getSleepAnalysisSamples(long startTime, + long endTime, + @NonNull InputKit.Result>> callback); + + /** + * Get blood pressure history + * + * @param startTime epoch for the start date of the range + * @param endTime epoch for the end date of the range + * @param callback {@link Result} containing a set history of user blood pressure + */ + public abstract void getBloodPressure(long startTime, + long endTime, + @NonNull Result> callback); + + /** + * Get blood weight history + * + * @param startTime epoch for the start date of the range + * @param endTime epoch for the end date of the range + * @param callback {@link Result} containing a set history of user weight + */ + public abstract void getWeight(long startTime, + long endTime, + @NonNull Result> callback); + + /** + * Start monitoring health sensors. + * + * @param sensorType sensor type should be one of these {@link SampleName} sensor + * @param samplingRate sensor sampling rate. + * Sensor will be started every X-Time Unit, for instance : { 5, {@link TimeUnit#MINUTES} }. + * If sampling rate is unspecified it will be set to 10 minute interval. + * @param listener {@link SensorListener} sensor listener + */ + public abstract void startMonitoring(@NonNull @SampleName String sensorType, + @NonNull Pair samplingRate, + @NonNull SensorListener listener); + + /** + * Stop monitoring health sensors. + * + * @param sensorType Sensor type should be one of these {@link SampleName} sensor + * @param listener {@link SensorListener} sensor listener + */ + public abstract void stopMonitoring(@NonNull @SampleName String sensorType, + @NonNull SensorListener listener); + + /** + * Start tracking specific sensor. + * + * @param sensorType Sample type should be one of these {@link SampleName} sensor + * @param samplingRate sensor sampling rate. + * Sensor will be started every X-Time Unit, for instance : { 5, {@link TimeUnit#MINUTES} }. + * If sampling rate is unspecified it will be set to 10 minute interval. + * @param listener {@link SensorListener} sensor listener + */ + public abstract void startTracking(@NonNull @SampleName String sensorType, + @NonNull Pair samplingRate, + @NonNull SensorListener listener); + + /** + * Stop tracking specific sensor. + * + * @param sensorType Sample type should be one of these {@link SampleName} sensor + * @param listener {@link SensorListener} sensor listener + */ + public abstract void stopTracking(@NonNull String sensorType, + @NonNull SensorListener listener); + + /** + * Stop all tracking specific sensor. + * + * @param listener {@link SensorListener} sensor listener + */ + public abstract void stopTrackingAll(@NonNull SensorListener listener); +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/HealthTrackerState.java b/android/src/main/java/nl/sense/rninputkit/inputkit/HealthTrackerState.java new file mode 100644 index 0000000..01fa5d1 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/HealthTrackerState.java @@ -0,0 +1,108 @@ +package nl.sense.rninputkit.inputkit; + +import android.content.Context; +import androidx.annotation.NonNull; +import android.text.TextUtils; +import android.util.Pair; + +import com.google.gson.JsonObject; + +import nl.sense.rninputkit.inputkit.constant.SampleType; +import nl.sense.rninputkit.inputkit.helper.PreferenceHelper; + +import static nl.sense.rninputkit.inputkit.constant.SampleType.SampleName; +import static nl.sense.rninputkit.inputkit.constant.SampleType.UNAVAILABLE; +import static nl.sense.rninputkit.inputkit.constant.SampleType.checkFitSampleType; + +/** + * Created by panjiyudasetya on 7/26/17. + */ + +public class HealthTrackerState { + private HealthTrackerState() { } + + /** + * Stored sensor state in shared preference + * + * @param context Current application context + * @param stateKey Tracker state key + * @param newSensorState New sensor state. + * Sample name should be one of available {@link SampleName} + * If it's unavailable it will throw {@link IllegalStateException} + */ + public static void save(@NonNull Context context, + @NonNull String stateKey, + @NonNull Pair newSensorState) { + validateState(newSensorState); + + JsonObject sensorsState = PreferenceHelper.getAsJson( + context, + stateKey + ); + + sensorsState.addProperty( + newSensorState.first, + newSensorState.second + ); + + // update preference + PreferenceHelper.add( + context, + stateKey, + sensorsState.toString() + ); + } + + /** + * Stored sensor state in shared preference + * + * @param context Current application context + * @param stateKey Tracker state key + * @param enables New sensor state + */ + public static void saveAll(@NonNull Context context, + @NonNull String stateKey, + @NonNull boolean enables) { + + JsonObject sensorsState = PreferenceHelper.getAsJson( + context, + stateKey + ); + + sensorsState.addProperty( + SampleType.STEP_COUNT, + enables + ); + + sensorsState.addProperty( + SampleType.DISTANCE_WALKING_RUNNING, + enables + ); + + // update preference + PreferenceHelper.add( + context, + stateKey, + sensorsState.toString() + ); + } + + /** + * Helper function to validate incoming sensor state. + * @param state New sensor state. + * Sample name should be one of available {@link SampleName} + * If it's unavailable it will throw {@link IllegalStateException} + * @throws IllegalStateException + */ + private static void validateState(@NonNull Pair state) + throws IllegalStateException { + @SampleName String sensorSample = state.first; + if (TextUtils.isEmpty(sensorSample) || checkFitSampleType(sensorSample).equals(UNAVAILABLE)) { + throw new IllegalStateException("INVALID_SENSOR_SAMPLE_TYPE!"); + } + + if (state.second == null) { + throw new IllegalStateException("UNSPECIFIED_SENSOR_STATE_VALUE!"); + } + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/InputKit.java b/android/src/main/java/nl/sense/rninputkit/inputkit/InputKit.java new file mode 100644 index 0000000..659812d --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/InputKit.java @@ -0,0 +1,349 @@ +package nl.sense.rninputkit.inputkit; + +import android.app.Activity; +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import android.util.Pair; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import nl.sense.rninputkit.inputkit.HealthProvider.IReleasableHostProvider; +import nl.sense.rninputkit.inputkit.HealthProvider.ProviderType; +import nl.sense.rninputkit.inputkit.HealthProvider.SensorListener; +import nl.sense.rninputkit.inputkit.constant.Interval; +import nl.sense.rninputkit.inputkit.constant.SampleType; +import nl.sense.rninputkit.inputkit.entity.BloodPressure; +import nl.sense.rninputkit.inputkit.entity.IKValue; +import nl.sense.rninputkit.inputkit.entity.SensorDataPoint; +import nl.sense.rninputkit.inputkit.entity.StepContent; +import nl.sense.rninputkit.inputkit.entity.Weight; +import nl.sense.rninputkit.inputkit.shealth.SamsungHealthProvider; +import nl.sense.rninputkit.inputkit.status.IKResultInfo; +import nl.sense.rninputkit.inputkit.status.IKProviderInfo; +import nl.sense.rninputkit.inputkit.googlefit.GoogleFitHealthProvider; + +/** + * Created by panjiyudasetya on 6/14/17. + */ + +public class InputKit implements IReleasableHostProvider { + private static InputKit sInputKit; + private HealthProvider mCurrentHealthProvider; + private GoogleFitHealthProvider mGoogleFitHealthProvider; + private SamsungHealthProvider mSamsungHealthProvider; + + /** + * A callback result for each Input Kit functionality. + * + * @param Expected result. + */ + public interface Result { + /** + * Callback function to handle new available data + * + * @param data Expected data + */ + void onNewData(T data); + + /** + * Callback function to handle any exceptions if any + * + * @param error {@link IKResultInfo} + */ + void onError(@NonNull IKResultInfo error); + } + + public interface Callback { + /** + * This action will be triggered when successfully connected to Input Kit Service. + * @param addMessages additional message + */ + void onAvailable(String... addMessages); + + /** + * This event will be triggered when Input Kit is not available for some reason. + * @param reason Typically contains error code and error message. + */ + void onNotAvailable(@NonNull IKResultInfo reason); + + /** + * This event will be triggered whenever connection to Input Kit service has been rejected. + * In any case, the problem probably solved by call + * {@link com.google.android.gms.common.ConnectionResult#startResolutionForResult(Activity, int)} + * which int value should be referred to + * {@link com.google.android.gms.common.ConnectionResult#getErrorCode()}. + * But this action required UI interaction, so be careful with it. + * @param connectionError {@link IKProviderInfo} + */ + void onConnectionRefused(@NonNull IKProviderInfo connectionError); + } + + private InputKit(@NonNull Context context) { + mGoogleFitHealthProvider = new GoogleFitHealthProvider(context, this); + mSamsungHealthProvider = new SamsungHealthProvider(context); + + // By default it will use Google Fit Health provider + mCurrentHealthProvider = mGoogleFitHealthProvider; + } + + /** + * Get instance of Input Kit class. + * + * @param context current application context + * @return {@link InputKit} + */ + public static InputKit getInstance(@NonNull Context context) { + if (sInputKit == null) sInputKit = new InputKit(context); + return sInputKit; + } + + /** + * Set current host activity. + * Typically it will be used to show an alert dialog since it bound to the Activity + * + * @param activity Current Host activity + */ + @SuppressWarnings("unused")//This is a public API + public void setHostActivity(@Nullable Activity activity) { + mCurrentHealthProvider.setHostActivity(activity); + } + + /** + * Set priority health provider + * @param healthProvider available health provider + */ + @SuppressWarnings("unused")//This is a public API + public void setHealthProvider(@NonNull ProviderType healthProvider) { + switch (healthProvider) { + case GOOGLE_FIT: mCurrentHealthProvider = mGoogleFitHealthProvider; + break; + case SAMSUNG_HEALTH: + mCurrentHealthProvider = mSamsungHealthProvider; + break; + case GARMIN_SDK: + default: + mCurrentHealthProvider = mGoogleFitHealthProvider; + break; + } + } + + /** + * Authorize Input Kit service connections. + * @param callback event listener + * @param permissionType permission type. in case specific handler required when asking input kit + * type. + * + */ + @SuppressWarnings("unused")//This is a public API + public void authorize(@NonNull Callback callback, String... permissionType) { + mCurrentHealthProvider.authorize(callback, permissionType); + } + + /** + * Disconnect from current Health provider + * @param callback {@link Result} event listener + */ + @SuppressWarnings("unused")//This is a public API + public void disconnectCurrentHealthProvider(@NonNull Result callback) { + mCurrentHealthProvider.disconnect(callback); + } + + /** + * Check health availability. + */ + @SuppressWarnings("unused")//This is a public API + public boolean isAvailable() { + return mCurrentHealthProvider.isAvailable(); + } + + /** + * Check authorised permission type availability. + * @param permissionType requested permission type + */ + @SuppressWarnings("unused")//This is a public API + public boolean isPermissionsAuthorised(String[] permissionType) { + return mCurrentHealthProvider.isPermissionsAuthorised(permissionType); + } + + /** + * Get total distance of walk on specific time range. + * + * @param startTime epoch for the start date + * @param endTime epoch for the end date + * @param limit historical data limitation + * set to 0 if you want to calculate all available distance within specific range + * @param callback {@link Result } containing number of total distance + */ + @SuppressWarnings("unused")//This is a public API + public void getDistance(long startTime, long endTime, int limit, @NonNull Result callback) { + mCurrentHealthProvider.getDistance(startTime, endTime, limit, callback); + } + + /** + * Get sample distance of walk on specific time range. + * @param startTime epoch for the start date + * @param endTime epoch for the end date + * @param limit historical data limitation + * set to 0 if you want to calculate all available distance within specific range + * @param callback {@link Result} containing set of available distance + */ + @SuppressWarnings("unused")//This is a public API + public void getDistanceSamples(long startTime, + long endTime, + int limit, + @NonNull Result>> callback) { + mCurrentHealthProvider.getDistanceSamples(startTime, endTime, limit, callback); + } + + /** + * Get total Today steps count. + * @param callback {@link Result } containing number of total steps count + */ + @SuppressWarnings("unused")//This is a public API + public void getStepCount(@NonNull Result callback) { + mCurrentHealthProvider.getStepCount(callback); + } + + /** + * Get total steps count of specific range. + * @param startTime epoch for the start date + * @param endTime epoch for the end date + * @param limit historical data limitation + * set to 0 if you want to calculate all available distance within specific range + * @param callback {@link Result } containing number of total steps count + */ + @SuppressWarnings("unused")//This is a public API + public void getStepCount(long startTime, + long endTime, + int limit, + @NonNull Result callback) { + mCurrentHealthProvider.getStepCount(startTime, endTime, limit, callback); + } + + /** + * Get distribution step count history by specific time period. + * This function should be called within asynchronous process because of + * reading historical data through {@link com.google.android.gms.fitness.Fitness#HistoryApi} will be executed on main + * thread by default. + * + * @param startTime epoch for the start date + * @param endTime epoch for the end date + * @param interval on of any {@link nl.sense.rninputkit.inputkit.constant.Interval.IntervalName} + * @param limit historical data limitation + * set to null if you want to calculate all available step count within specific range + * @param callback {@link Result } containing a set of history step content + */ + @SuppressWarnings("unused")//This is a public API + public void getStepCountDistribution(long startTime, + long endTime, + @NonNull @Interval.IntervalName String interval, + int limit, + @NonNull Result callback) { + mCurrentHealthProvider.getStepCountDistribution(startTime, endTime, interval, limit, callback); + } + + /** + * Returns data contains sleep analysis data of a specific range. Sorted recent data first. + * @param startTime epoch for the start date of the range + * @param endTime epoch for the end date of the range + * @param callback {@link Result} containing a set of sleep analysis samples + */ + public void getSleepAnalysisSamples(long startTime, long endTime, + @NonNull InputKit.Result>> callback) { + mCurrentHealthProvider.getSleepAnalysisSamples(startTime, endTime, callback); + } + + /** + * Get blood pressure history + * @param startTime epoch for the start date of the range + * @param endTime epoch for the end date of the range + * @param callback {@link Result} containing a set history of user blood pressure + */ + public void getBloodPressure(long startTime, long endTime, @NonNull Result> callback) { + mCurrentHealthProvider.getBloodPressure(startTime, endTime, callback); + } + + /** + * Get weight history + * @param startTime epoch for the start date of the range + * @param endTime epoch for the end date of the range + * @param callback {@link Result} containing a set history of user weight + */ + public void getWeight(long startTime, long endTime, @NonNull Result> callback) { + mCurrentHealthProvider.getWeight(startTime, endTime, callback); + } + + /* Start monitoring health sensors. + * @param sensorType sensor type should be one of these {@link SampleType.SampleName} sensor + * @param samplingRate sensor sampling rate. + * Sensor will be started every X-Time Unit, for instance : { 5, {@link TimeUnit#MINUTES} }. + * If sampling rate is unspecified it will be set to 10 minute interval. + * @param listener {@link SensorListener} sensor listener + */ + @SuppressWarnings("unused")//This is a public API + public void startMonitoring(@NonNull String sensorType, + @NonNull Pair samplingRate, + @NonNull SensorListener listener) { + mCurrentHealthProvider.startMonitoring(sensorType, samplingRate, listener); + } + + /** + * Stop monitoring health sensors. + * @param sensorType Sensor type should be one of these {@link SampleType.SampleName} sensor + * @param listener {@link SensorListener} sensor listener + */ + @SuppressWarnings("unused")//This is a public API + public void stopMonitoring(@NonNull String sensorType, + @NonNull SensorListener listener) { + mCurrentHealthProvider.stopMonitoring(sensorType, listener); + } + + /** + * Start tracking specific sensor. + * + * @param sensorType Sample type should be one of these {@link SampleType.SampleName} sensor + * @param samplingRate sensor sampling rate. + * Sensor will be started every X-Time Unit, for instance : { 5, {@link TimeUnit#MINUTES} }. + * If sampling rate is unspecified it will be set to 10 minute interval. + * @param listener {@link SensorListener} sensor listener + */ + @SuppressWarnings("unused")//This is a public API + public void startTracking(@NonNull @SampleType.SampleName String sensorType, + @NonNull Pair samplingRate, + @NonNull SensorListener listener) { + mCurrentHealthProvider.startTracking(sensorType, samplingRate, listener); + } + + /** + * Stop tracking specific sensor. + * + * @param sensorType Sample type should be one of these {@link SampleType.SampleName} sensor + * @param listener {@link SensorListener} sensor listener + */ + @SuppressWarnings("unused")//This is a public API + public void stopTracking(@NonNull String sensorType, + @NonNull SensorListener listener) { + mCurrentHealthProvider.stopTracking(sensorType, listener); + } + + /** + * Stop all tracking specific sensor. + * + * @param listener {@link SensorListener} sensor listener + */ + @SuppressWarnings("unused")//This is a public API + public void stopTrackingAll(@NonNull SensorListener listener) { + mCurrentHealthProvider.stopTrackingAll(listener); + } + + @Override + public void release() { + sInputKit = null; + mCurrentHealthProvider = null; + mGoogleFitHealthProvider = null; + mSamsungHealthProvider = null; + // TODO: Put another references which should be released. + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/Options.java b/android/src/main/java/nl/sense/rninputkit/inputkit/Options.java new file mode 100644 index 0000000..21dfd40 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/Options.java @@ -0,0 +1,151 @@ +package nl.sense.rninputkit.inputkit; + +import nl.sense.rninputkit.inputkit.constant.Interval; +import nl.sense.rninputkit.inputkit.entity.TimeInterval; + +import static nl.sense.rninputkit.inputkit.Options.Validator.validateEndTime; +import static nl.sense.rninputkit.inputkit.Options.Validator.validateStartTime; + +/** + * Created by panjiyudasetya on 6/19/17. + */ + +public class Options { + private static final TimeInterval DEFAULT_TIME_INTERVAL = new TimeInterval(Interval.TEN_MINUTE); + + private Long startTime; + private Long endTime; + private boolean useDataAggregation; + private TimeInterval timeInterval; + private Integer limitation; + + private Options(Long startTime, + Long endTime, + boolean useDataAggregation, + TimeInterval timeInterval, + Integer limitation) { + this.startTime = startTime; + this.endTime = endTime; + this.useDataAggregation = useDataAggregation; + this.timeInterval = timeInterval; + this.limitation = limitation; + } + + public Long getStartTime() { + return startTime; + } + + public Long getEndTime() { + return endTime; + } + + public boolean isUseDataAggregation() { + return useDataAggregation; + } + + public TimeInterval getTimeInterval() { + return timeInterval; + } + + public Integer getLimitation() { + return limitation; + } + + public static class Builder { + private Long newStartTime; + private Long newEndTime; + private boolean newUseDataAggregation; + private TimeInterval newTimeInterval; + private Integer newLimitation; + + /** + * Set start time of steps history. + * @param startTime epoch + * @return Builder Options Builder + */ + public Builder startTime(Long startTime) { + this.newStartTime = startTime; + return this; + } + + /** + * Set end time of steps history. + * @param endTime epoch + * @return Builder Options Builder + */ + public Builder endTime(Long endTime) { + this.newEndTime = endTime; + return this; + } + + /** + * It will aggregating steps count data history by specific time and time unit. + * @return Builder Options Builder + */ + public Builder useDataAggregation() { + this.newUseDataAggregation = true; + return this; + } + + /** + * If {@link TimeInterval} not provided it will be set to {@link Options#DEFAULT_TIME_INTERVAL}. + * @param timeInterval time interval + * @return Builder Options Builder + */ + public Builder timeInterval(TimeInterval timeInterval) { + this.newTimeInterval = timeInterval; + return this; + } + + /** + * Set data limitation if required. + * @param limitation data limitation + * @return Builder Options Builder + */ + public Builder limitation(Integer limitation) { + this.newLimitation = limitation; + return this; + } + + public Options build() { + newStartTime = validateStartTime(newStartTime); + newEndTime = validateEndTime(newStartTime, newEndTime); + + return new Options(newStartTime, + newEndTime, + newUseDataAggregation, + newTimeInterval == null ? DEFAULT_TIME_INTERVAL : newTimeInterval, + (newLimitation == null || newLimitation <= 0) ? null : newLimitation + ); + } + } + + static class Validator { + /** + * Validate start time value. If lower than 0, it will be set to 0 + * @param startTime epoch + * @return valid start time + */ + static long validateStartTime(Long startTime) { + if (startTime == null) throw new IllegalStateException("Start time should be defined!"); + return startTime < 0 ? 0 : startTime; + } + + /** + * Validate end time value. If end time lower than 0, it will be set to 0 + * + * @param startTime epoch + * @param endTime epoch + * @return valid end time + * @throws IllegalStateException if end time lower than start time + */ + static long validateEndTime(Long startTime, Long endTime) { + if (endTime == null) throw new IllegalStateException("End time should be defined!"); + + endTime = endTime < 0 ? 0 : endTime; + + if (endTime < startTime) throw new IllegalStateException("End time cannot be lower than start time!"); + return endTime; + } + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/constant/ApiPermissions.java b/android/src/main/java/nl/sense/rninputkit/inputkit/constant/ApiPermissions.java new file mode 100644 index 0000000..69bfe95 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/constant/ApiPermissions.java @@ -0,0 +1,17 @@ +package nl.sense.rninputkit.inputkit.constant; + +import static android.Manifest.permission.ACCESS_FINE_LOCATION; +import static android.Manifest.permission.INTERNET; + +/** + * Created by panjiyudasetya on 7/5/17. + */ + +public class ApiPermissions { + private ApiPermissions() { } + + public static final String[] STEPS_API_PERMISSIONS = { + INTERNET, + ACCESS_FINE_LOCATION + }; +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/constant/Constant.java b/android/src/main/java/nl/sense/rninputkit/inputkit/constant/Constant.java new file mode 100644 index 0000000..686cc8b --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/constant/Constant.java @@ -0,0 +1,11 @@ +package nl.sense.rninputkit.inputkit.constant; + +/** + * Created by panjiyudasetya on 10/17/17. + */ + +public class Constant { + private Constant() { } + public static final String MONITORED_HEALTH_SENSORS = "MONITORED_HEALTH_SENSORS"; + public static final String TRACKED_HEALTH_SENSORS = "TRACKED_HEALTH_SENSORS"; +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/constant/DataSampling.java b/android/src/main/java/nl/sense/rninputkit/inputkit/constant/DataSampling.java new file mode 100644 index 0000000..a90a3ea --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/constant/DataSampling.java @@ -0,0 +1,13 @@ +package nl.sense.rninputkit.inputkit.constant; + +import java.util.concurrent.TimeUnit; + +/** + * Created by panjiyudasetya on 6/15/17. + */ + +public class DataSampling { + private DataSampling() { } + public static final int DEFAULT_TIME_SAMPLING_RATE = 10; + public static final TimeUnit DEFAULT_SAMPLING_TIME_UNIT = TimeUnit.MINUTES; +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/constant/IKStatus.java b/android/src/main/java/nl/sense/rninputkit/inputkit/constant/IKStatus.java new file mode 100644 index 0000000..4112b48 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/constant/IKStatus.java @@ -0,0 +1,44 @@ +package nl.sense.rninputkit.inputkit.constant; + +/** + * Created by panjiyudasetya on 10/12/17. + */ + +public class IKStatus { + private IKStatus() { } + public static final String INPUT_KIT_DISCONNECTED = "INPUT_KIT_DISCONNECTED"; + public static final String INPUT_KIT_NOT_CONNECTED = "INPUT_KIT_NOT_CONNECTED"; + public static final String INPUT_KIT_SERVICE_NOT_AVAILABLE = "INPUT_KIT_SERVICE_NOT_AVAILABLE"; + public static final String REQUIRED_GOOGLE_FIT_APP = "REQUIRED_GOOGLE_FIT_APP"; + public static final String INPUT_KIT_OUT_OF_DATE_PLAY_SERVICE = "INPUT_KIT_OUT_OF_DATE_PLAY_SERVICE"; + public static final String INPUT_KIT_REQUIRED_GRANTED_PERMISSIONS = "INPUT_KIT_REQUIRED_GRANTED_PERMISSIONS"; + public static final String INPUT_KIT_CONNECTION_ERROR = "INPUT_KIT_CONNECTION_ERROR"; + public static final String INPUT_KIT_NO_DEVICES_SOURCE = "INPUT_KIT_NO_DEVICES_SOURCE"; + public static final String INPUT_KIT_MONITOR_REGISTERED = "INPUT_KIT_MONITOR_ALREADY_REGISTERED"; + public static final String INPUT_KIT_MONITOR_UNREGISTERED = "INPUT_KIT_MONITOR_UNREGISTERED"; + public static final String INPUT_KIT_MONITORING_NOT_AVAILABLE = "INPUT_KIT_MONITORING_NOT_AVAILABLE"; + public static final String INPUT_KIT_UNREACHABLE_CONTEXT = + String.format("UNREACHABLE_APPLICATION_CONTEXT \n%s %s", + "Context was no longer maintained in memory, ", + "you might need to re-initiate InputKit instance before use any apis."); + public static final String SAMSUNG_HEALTH_IS_NOT_AVAILABLE = "SAMSUNG_HEALTH_IS_NOT_AVAILABLE"; + public static final String SAMSUNG_HEALTH_NOT_INSTALLED = "SAMSUNG_HEALTH_NOT_INSTALLED"; + public static final String SAMSUNG_HEALTH_OLD_VERSION = "SAMSUNG_HEALTH_OLD_VERSION"; + public static final String SAMSUNG_HEALTH_DISABLED = "SAMSUNG_HEALTH_DISABLED"; + public static final String SAMSUNG_HEALTH_USER_AGREEMENT_NEEDED = "SAMSUNG_HEALTH_USER_AGREEMENT_NEEDED"; + + public abstract class Code { + public static final int VALID_REQUEST = 0; + public static final int UNKNOWN_ERROR = -99; + public static final int IK_NOT_CONNECTED = -3; + public static final int IK_NOT_AVAILABLE = -4; + public static final int GOOGLE_FIT_REQUIRED = -5; + public static final int OUT_OF_DATE_PLAY_SERVICE = -6; + public static final int INVALID_REQUEST = -7; + public static final int REQUIRED_GRANTED_PERMISSIONS = -8; + + public static final int S_HEALTH_PERMISSION_REQUIRED = -9; + public static final int S_HEALTH_DISCONNECTED = -10; + public static final int S_HEALTH_CONNECTION_ERROR = -11; + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/constant/Interval.java b/android/src/main/java/nl/sense/rninputkit/inputkit/constant/Interval.java new file mode 100644 index 0000000..ffb3d38 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/constant/Interval.java @@ -0,0 +1,31 @@ +package nl.sense.rninputkit.inputkit.constant; + +import androidx.annotation.StringDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Created by panjiyudasetya on 6/19/17. + */ + +public class Interval { + private Interval() { } + + @Retention(RetentionPolicy.SOURCE) + @StringDef({ + ONE_WEEK, + ONE_DAY, + AN_HOUR, + HALF_HOUR, + TEN_MINUTE, + ONE_MINUTE + }) + public @interface IntervalName { } + public static final String ONE_WEEK = "week"; + public static final String ONE_DAY = "day"; + public static final String AN_HOUR = "hour"; + public static final String HALF_HOUR = "halfHour"; + public static final String TEN_MINUTE = "tenMinute"; + public static final String ONE_MINUTE = "oneMinute"; +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/constant/RequiredApp.java b/android/src/main/java/nl/sense/rninputkit/inputkit/constant/RequiredApp.java new file mode 100644 index 0000000..dfe5065 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/constant/RequiredApp.java @@ -0,0 +1,12 @@ +package nl.sense.rninputkit.inputkit.constant; + +/** + * Created by panjiyudasetya on 9/26/17. + */ + +public class RequiredApp { + private RequiredApp() { } + public static final String GOOGLE_FIT_PACKAGE_NAME = "com.google.android.apps.fitness"; + public static final String PLAY_SERVICE_PACKAGE_NAME = "com.google.android.gms"; + public static final String SAMSUNG_HEALTH_PACKAGE_NAME = "com.sec.android.app.shealth"; +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/constant/SampleType.java b/android/src/main/java/nl/sense/rninputkit/inputkit/constant/SampleType.java new file mode 100644 index 0000000..2803c96 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/constant/SampleType.java @@ -0,0 +1,42 @@ +package nl.sense.rninputkit.inputkit.constant; + +import androidx.annotation.NonNull; +import androidx.annotation.StringDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * This is a constants value to define request read permissions for the given SampleType(s). + * + * Created by panjiyudasetya on 6/19/17. + */ + +public class SampleType { + private SampleType() { } + + @Retention(RetentionPolicy.SOURCE) + @StringDef({ + SLEEP, + STEP_COUNT, + DISTANCE_WALKING_RUNNING, + WEIGHT, + BLOOD_PRESSURE + }) + public @interface SampleName { } + public static final String SLEEP = "sleep"; + public static final String STEP_COUNT = "stepCount"; + public static final String DISTANCE_WALKING_RUNNING = "distanceWalkingRunning"; + public static final String WEIGHT = "weight"; + public static final String BLOOD_PRESSURE = "bloodPressure"; + public static final String UNAVAILABLE = "unavailable"; + + public static String checkFitSampleType(@NonNull String sampleType) { + // Sleep is not supported by GoogleFit at this moment + if (sampleType.equals(STEP_COUNT) || sampleType.equals(DISTANCE_WALKING_RUNNING) + || sampleType.equals(WEIGHT)) { + return sampleType; + } + return UNAVAILABLE; + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/entity/BloodPressure.java b/android/src/main/java/nl/sense/rninputkit/inputkit/entity/BloodPressure.java new file mode 100644 index 0000000..344b5e3 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/entity/BloodPressure.java @@ -0,0 +1,94 @@ +package nl.sense.rninputkit.inputkit.entity; + +import com.google.gson.Gson; +import com.google.gson.annotations.Expose; + +/** + * Created by xedi on 10/9/17. + */ + +public class BloodPressure { + private static final Gson GSON = new Gson(); + @Expose + private Integer systolic; + @Expose + private Integer diastolic; + @Expose + private Float mean; + @Expose + private Integer pulse; + @Expose + private String comment; + @Expose + private DateContent timeRecord; + + public BloodPressure(Integer sys, Integer dia, Long time) { + this.systolic = sys; + this.diastolic = dia; + this.timeRecord = new DateContent(time); + } + + public Integer getSystolic() { + return systolic; + } + + public void setSystolic(Integer systolic) { + this.systolic = systolic; + } + + public Integer getDiastolic() { + return diastolic; + } + + public void setDiastolic(Integer diastolic) { + this.diastolic = diastolic; + } + + public Float getMean() { + return mean; + } + + public void setMean(Float mean) { + this.mean = mean; + } + + public Integer getPulse() { + return pulse; + } + + public void setPulse(Integer pulse) { + this.pulse = pulse; + } + + public String getComment() { + return comment; + } + + public void setComment(String comment) { + this.comment = comment; + } + + public DateContent getTimeRecord() { + return timeRecord; + } + + public void setTimeRecord(DateContent timeRecord) { + this.timeRecord = timeRecord; + } + + public String toJson() { + return GSON.toJson(this); + } + + @Override + public String toString() { + return "{" + + "time: " + timeRecord.getString() + + "\nsystolic=" + systolic + + "\n, diastolic=" + diastolic + + "\n, mean=" + mean + + "\n, pulse=" + pulse + + "\n, comment=" + comment + + "}"; + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/entity/DateContent.java b/android/src/main/java/nl/sense/rninputkit/inputkit/entity/DateContent.java new file mode 100644 index 0000000..a6ea09a --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/entity/DateContent.java @@ -0,0 +1,61 @@ +package nl.sense.rninputkit.inputkit.entity; + +import com.google.gson.annotations.Expose; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +/** + * Created by panjiyudasetya on 7/6/17. + */ + +public class DateContent { + private static final String STR_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss Z"; + private static final DateFormat DATE_FORMATTER = new SimpleDateFormat(STR_DATE_FORMAT, Locale.US); + @Expose + private long epoch; + @Expose + private String string; + + public DateContent(long epoch) { + this.epoch = epoch; + this.string = DATE_FORMATTER.format(new Date(epoch)); + } + + public long getEpoch() { + return epoch; + } + + public String getString() { + return string; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof DateContent)) return false; + + DateContent that = (DateContent) o; + + if (epoch != that.epoch) return false; + return string != null ? string.equals(that.string) : that.string == null; + + } + + @Override + public int hashCode() { + int result = (int) (epoch ^ (epoch >>> 32)); + result = 31 * result + (string != null ? string.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "DateContent{" + + "epoch=" + epoch + + ", string='" + string + '\'' + + '}'; + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/entity/IKValue.java b/android/src/main/java/nl/sense/rninputkit/inputkit/entity/IKValue.java new file mode 100644 index 0000000..acda51f --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/entity/IKValue.java @@ -0,0 +1,116 @@ +package nl.sense.rninputkit.inputkit.entity; + +import androidx.annotation.NonNull; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.annotations.Expose; + +import java.util.List; +import java.util.Objects; + +/** + * Created by panjiyudasetya on 10/23/17. + */ + +public class IKValue { + protected static final Gson GSON = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); + @Expose + protected T value; + @Expose + protected DateContent startDate; + @Expose + protected DateContent endDate; + @Expose(serialize = false) + private boolean flagOverlap; + + public IKValue(T value) { + this.value = value; + } + + public IKValue(@NonNull T value, + @NonNull DateContent startDate, + @NonNull DateContent endDate) { + this.value = value; + this.startDate = startDate; + this.endDate = endDate; + } + + public IKValue(@NonNull DateContent startDate, + @NonNull DateContent endDate) { + this.startDate = startDate; + this.endDate = endDate; + } + + public T getValue() { + return value; + } + + public void setValue(T value) { + this.value = value; + } + + public DateContent getStartDate() { + return startDate; + } + + public DateContent getEndDate() { + return endDate; + } + + public boolean isFlaggedOverlap() { + return flagOverlap; + } + + public void setFlagOverlap(boolean flagOverlap) { + this.flagOverlap = flagOverlap; + } + + public static int getTotalIntegers(List> values) { + if (values == null || values.isEmpty()) return 0; + int total = 0; + for (IKValue value : values) { + total += value.getValue(); + } + return total; + } + + public static float getTotalFloats(List> values) { + if (values == null || values.isEmpty()) return 0; + float total = 0; + for (IKValue value : values) { + total += value.getValue(); + } + return total; + } + + public String toJson() { + return GSON.toJson(this); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + IKValue ikValue = (IKValue) o; + return flagOverlap == ikValue.flagOverlap + && Objects.equals(value, ikValue.value) + && Objects.equals(startDate, ikValue.startDate) + && Objects.equals(endDate, ikValue.endDate); + } + + @Override + public int hashCode() { + return Objects.hash(value, startDate, endDate, flagOverlap); + } + + @Override + public String toString() { + return "IKValue{" + + "value=" + value + + ", startDate=" + startDate + + ", endDate=" + endDate + + ", flagOverlap=" + flagOverlap + + '}'; + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/entity/SensorDataPoint.java b/android/src/main/java/nl/sense/rninputkit/inputkit/entity/SensorDataPoint.java new file mode 100644 index 0000000..5edb2a4 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/entity/SensorDataPoint.java @@ -0,0 +1,36 @@ +package nl.sense.rninputkit.inputkit.entity; + +import androidx.annotation.NonNull; + +import java.util.List; + +/** + * Created by panjiyudasetya on 10/20/17. + */ + +public class SensorDataPoint { + public String topic; + public List> payload; + + public SensorDataPoint(@NonNull String topic, + @NonNull List> payload) { + this.topic = topic; + this.payload = payload; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public List> getPayload() { + return payload; + } + + public void setPayload(List> payload) { + this.payload = payload; + } +} \ No newline at end of file diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/entity/Step.java b/android/src/main/java/nl/sense/rninputkit/inputkit/entity/Step.java new file mode 100644 index 0000000..acebfa7 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/entity/Step.java @@ -0,0 +1,11 @@ +package nl.sense.rninputkit.inputkit.entity; + +/** + * Created by panjiyudasetya on 6/15/17. + */ + +public class Step extends IKValue { + public Step(int value, long startDate, long endDate) { + super(value, new DateContent(startDate), new DateContent(endDate)); + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/entity/StepContent.java b/android/src/main/java/nl/sense/rninputkit/inputkit/entity/StepContent.java new file mode 100644 index 0000000..85d2203 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/entity/StepContent.java @@ -0,0 +1,52 @@ +package nl.sense.rninputkit.inputkit.entity; + +import androidx.annotation.NonNull; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.annotations.Expose; + +import java.util.List; +import java.util.Objects; + +/** + * Created by panjiyudasetya on 6/15/17. + */ + +public class StepContent extends IKValue> { + private static final Gson GSON = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); + @Expose(serialize = false) + private boolean isQueryOk; + + public StepContent(boolean isQueryOk, + long startDate, + long endDate, + @NonNull List steps) { + super(steps, new DateContent(startDate), new DateContent(endDate)); + this.isQueryOk = isQueryOk; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + StepContent that = (StepContent) o; + return isQueryOk == that.isQueryOk; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), isQueryOk); + } + + @Override + public String toString() { + return "StepContent{" + + "isQueryOk=" + isQueryOk + + ", value=" + value + + ", startDate=" + startDate + + ", endDate=" + endDate + + '}'; + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/entity/TimeInterval.java b/android/src/main/java/nl/sense/rninputkit/inputkit/entity/TimeInterval.java new file mode 100644 index 0000000..f36ed74 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/entity/TimeInterval.java @@ -0,0 +1,87 @@ +package nl.sense.rninputkit.inputkit.entity; + +import androidx.annotation.NonNull; + +import java.util.concurrent.TimeUnit; + +import nl.sense.rninputkit.inputkit.constant.Interval; + +import static nl.sense.rninputkit.inputkit.constant.Interval.ONE_DAY; +import static nl.sense.rninputkit.inputkit.constant.Interval.HALF_HOUR; +import static nl.sense.rninputkit.inputkit.constant.Interval.AN_HOUR; +import static nl.sense.rninputkit.inputkit.constant.Interval.ONE_MINUTE; +import static nl.sense.rninputkit.inputkit.constant.Interval.TEN_MINUTE; +import static nl.sense.rninputkit.inputkit.constant.Interval.ONE_WEEK; + +/** + * Created by panjiyudasetya on 6/19/17. + */ + +public class TimeInterval { + private int mValue; + private TimeUnit mTimeUnit; + + public TimeInterval(@NonNull @Interval.IntervalName String type) { + setValue(type); + } + + public int getValue() { + return mValue; + } + + public TimeUnit getTimeUnit() { + return mTimeUnit; + } + + private void setValue(@Interval.IntervalName String type) { + if (type.equals(ONE_WEEK)) { + mValue = 7; + mTimeUnit = TimeUnit.DAYS; + } else if (type.equals(ONE_DAY)) { + mValue = 1; + mTimeUnit = TimeUnit.DAYS; + } else if (type.equals(AN_HOUR)) { + mValue = 1; + mTimeUnit = TimeUnit.HOURS; + } else if (type.equals(HALF_HOUR)) { + mValue = 30; + mTimeUnit = TimeUnit.MINUTES; + } else if (type.equals(TEN_MINUTE)) { + mValue = 10; + mTimeUnit = TimeUnit.MINUTES; + } else if (type.equals(ONE_MINUTE)) { + mValue = 1; + mTimeUnit = TimeUnit.MINUTES; + } else { + mValue = 1; + mTimeUnit = TimeUnit.DAYS; + } + } + + @Override + public String toString() { + return "TimeInterval{" + + "value=" + mValue + + ", timeUnit=" + mTimeUnit + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof TimeInterval)) return false; + + TimeInterval that = (TimeInterval) o; + + if (mValue != that.mValue) return false; + return mTimeUnit == that.mTimeUnit; + + } + + @Override + public int hashCode() { + int result = mValue; + result = 31 * result + (mTimeUnit != null ? mTimeUnit.hashCode() : 0); + return result; + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/entity/Weight.java b/android/src/main/java/nl/sense/rninputkit/inputkit/entity/Weight.java new file mode 100644 index 0000000..50170d0 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/entity/Weight.java @@ -0,0 +1,72 @@ +package nl.sense.rninputkit.inputkit.entity; + +import com.google.gson.Gson; +import com.google.gson.annotations.Expose; + +/** + * Created by xedi on 10/9/17. + */ + +public class Weight { + private static final Gson GSON = new Gson(); + @Expose + private DateContent timeRecord; + @Expose + private Float weight; + @Expose + private Integer bodyFat; + @Expose + private String comment; + + public Weight(Float weight, Integer bodyFat, long time) { + this.weight = weight; + this.bodyFat = bodyFat; + this.timeRecord = new DateContent(time); + } + + public DateContent getTimeRecorded() { + return timeRecord; + } + + public void setTimeRecorded(DateContent timeRecord) { + this.timeRecord = timeRecord; + } + + public Float getWeight() { + return weight; + } + + public void setWeight(Float weight) { + this.weight = weight; + } + + public Integer getBodyFat() { + return bodyFat; + } + + public void setBodyFat(Integer bodyFat) { + this.bodyFat = bodyFat; + } + + public String getComment() { + return comment; + } + + public void setComment(String comment) { + this.comment = comment; + } + + public String toJson() { + return GSON.toJson(this); + } + + @Override + public String toString() { + return "{" + + "time: " + timeRecord.getString() + + "\nweight=" + weight + + "\n, bodyFat=" + bodyFat + + "\n, comment=" + comment + + "}"; + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/FitPermissionSet.java b/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/FitPermissionSet.java new file mode 100644 index 0000000..90f733d --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/FitPermissionSet.java @@ -0,0 +1,67 @@ +package nl.sense.rninputkit.inputkit.googlefit; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.google.android.gms.fitness.FitnessOptions; +import com.google.android.gms.fitness.data.DataType; + +import nl.sense.rninputkit.inputkit.constant.SampleType; + +public class FitPermissionSet { + private static FitPermissionSet sPermissionSet; + + FitPermissionSet() { } + + public static FitPermissionSet getInstance() { + if (sPermissionSet == null) { + sPermissionSet = new FitPermissionSet(); + } + return sPermissionSet; + } + + public FitnessOptions getPermissionsSet(@Nullable String[] sampleTypes) { + FitnessOptions.Builder builder = FitnessOptions.builder(); + if (sampleTypes != null && sampleTypes.length > 0) { + for (String sampleType : sampleTypes) { + createFitnessOptions(sampleType, builder); + } + } + return builder.build(); + } + + private void createFitnessOptions(@NonNull String sampleType, + @NonNull FitnessOptions.Builder builder) { + if (sampleType.equals(SampleType.STEP_COUNT)) { + builder.addDataType( + DataType.TYPE_STEP_COUNT_DELTA, + FitnessOptions.ACCESS_READ + ).addDataType( + DataType.AGGREGATE_STEP_COUNT_DELTA, + FitnessOptions.ACCESS_READ + ); + return; + } + + if (sampleType.equals(SampleType.DISTANCE_WALKING_RUNNING)) { + builder.addDataType( + DataType.TYPE_DISTANCE_DELTA, + FitnessOptions.ACCESS_READ + ).addDataType( + DataType.AGGREGATE_DISTANCE_DELTA, + FitnessOptions.ACCESS_READ + ); + return; + } + + if (sampleType.equals(SampleType.WEIGHT)) { + builder.addDataType( + DataType.TYPE_WEIGHT, + FitnessOptions.ACCESS_READ + ).addDataType( + DataType.AGGREGATE_WEIGHT_SUMMARY, + FitnessOptions.ACCESS_READ + ); + } + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/GoogleFitHealthProvider.java b/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/GoogleFitHealthProvider.java new file mode 100644 index 0000000..86059f9 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/GoogleFitHealthProvider.java @@ -0,0 +1,744 @@ +package nl.sense.rninputkit.inputkit.googlefit; + +import android.content.Context; +import android.content.Intent; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import android.util.Log; +import android.util.Pair; + +import com.google.android.gms.auth.api.signin.GoogleSignIn; +import com.google.android.gms.auth.api.signin.GoogleSignInAccount; +import com.google.android.gms.auth.api.signin.GoogleSignInClient; +import com.google.android.gms.auth.api.signin.GoogleSignInOptions; +import com.google.android.gms.fitness.Fitness; +import com.google.android.gms.fitness.FitnessOptions; +import com.google.android.gms.fitness.request.DataReadRequest; +import com.google.android.gms.tasks.Continuation; +import com.google.android.gms.tasks.OnCompleteListener; +import com.google.android.gms.tasks.Task; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import nl.sense.rninputkit.inputkit.HealthProvider; +import nl.sense.rninputkit.inputkit.HealthTrackerState; +import nl.sense.rninputkit.inputkit.InputKit.Callback; +import nl.sense.rninputkit.inputkit.InputKit.Result; +import nl.sense.rninputkit.inputkit.Options; +import nl.sense.rninputkit.inputkit.constant.Constant; +import nl.sense.rninputkit.inputkit.constant.IKStatus; +import nl.sense.rninputkit.inputkit.constant.Interval; +import nl.sense.rninputkit.inputkit.constant.SampleType; +import nl.sense.rninputkit.inputkit.entity.IKValue; +import nl.sense.rninputkit.inputkit.entity.SensorDataPoint; +import nl.sense.rninputkit.inputkit.entity.StepContent; +import nl.sense.rninputkit.inputkit.entity.TimeInterval; +import nl.sense.rninputkit.inputkit.googlefit.history.FitHistory; +import nl.sense.rninputkit.inputkit.googlefit.sensor.SensorManager; +import nl.sense.rninputkit.inputkit.helper.AppHelper; +import nl.sense.rninputkit.inputkit.helper.InputKitTimeUtils; +import nl.sense.rninputkit.inputkit.status.IKProviderInfo; +import nl.sense.rninputkit.inputkit.status.IKResultInfo; + +import static nl.sense.rninputkit.inputkit.constant.IKStatus.Code.IK_NOT_AVAILABLE; + +/** + * Created by panjiyudasetya on 10/13/17. + */ + +public class GoogleFitHealthProvider extends HealthProvider { + public static final int GF_PERMISSION_REQUEST_CODE = 77; + private static final IKResultInfo OUT_OF_DATE_PLAY_SERVICE = new IKResultInfo( + IKStatus.Code.OUT_OF_DATE_PLAY_SERVICE, + IKStatus.INPUT_KIT_OUT_OF_DATE_PLAY_SERVICE + ); + private static final IKResultInfo REQUIRED_GOOGLE_FIT_APP = new IKResultInfo( + IKStatus.Code.GOOGLE_FIT_REQUIRED, + IKStatus.REQUIRED_GOOGLE_FIT_APP + ); + private static final String TAG = GoogleFitHealthProvider.class.getSimpleName(); + private FitHistory mFitHistory; + private SensorManager mSensorMonitoring; + private SensorManager mSensorTracking; + + public GoogleFitHealthProvider(@NonNull Context context) { + super(context); + init(context); + } + + public GoogleFitHealthProvider(@NonNull Context context, @NonNull IReleasableHostProvider releasableHost) { + super(context, releasableHost); + init(context); + } + + private void init(@NonNull Context context) { + mFitHistory = new FitHistory(context); + mSensorMonitoring = new SensorManager(context); + mSensorTracking = new SensorManager(context); + } + + @Override + public boolean isAvailable() { + return getContext() != null && GoogleSignIn.hasPermissions(GoogleSignIn.getLastSignedInAccount(getContext())); + } + + @Override + public boolean isPermissionsAuthorised(String[] permissionTypes) { + if (getContext() != null && permissionTypes != null && permissionTypes.length > 0) { + FitnessOptions options = FitPermissionSet.getInstance().getPermissionsSet(permissionTypes); + return GoogleSignIn.hasPermissions( + GoogleSignIn.getLastSignedInAccount(getContext()), + options); + } + return isAvailable(); + } + + @Override + public void authorize(@NonNull final Callback callback, String... permissionType) { + Context context = getContext(); + if (context == null) { + onUnreachableContext(callback); + return; + } + + if (!AppHelper.isPlayServiceUpToDate(context)) { + callback.onNotAvailable(OUT_OF_DATE_PLAY_SERVICE); + return; + } + + if (!AppHelper.isGoogleFitInstalled(context)) { + callback.onNotAvailable(REQUIRED_GOOGLE_FIT_APP); + return; + } + + if (!isPermissionsAuthorised(permissionType)) { + if (getHostActivity() == null) { + onUnreachableContext(callback); + return; + } + + startSignedInAndAskForPermission(permissionType); + + callback.onConnectionRefused(new IKProviderInfo( + IKStatus.Code.REQUIRED_GRANTED_PERMISSIONS, + IKStatus.INPUT_KIT_REQUIRED_GRANTED_PERMISSIONS)); + return; + } + + callback.onAvailable("CONNECTED_TO_GOOGLE_FIT"); + } + + @Override + public void disconnect(@NonNull final Result callback) { + final Context context = getContext(); + if (isInvalidContext(context, callback)) return; + if (!isAvailable(callback)) return; + + assert context != null; + final GoogleSignInAccount account = GoogleSignIn.getLastSignedInAccount(context); + if (account != null) { + // Disconnect from Fit App and revoke existing permission access. + Fitness.getConfigClient(context, account).disableFit() + .continueWithTask(new Continuation>() { + @Override + public Task then(@NonNull Task task) { + return GoogleSignIn.getClient(context, getOptions()) + .revokeAccess(); + } + }) + .addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + callback.onNewData(true); + } + }); + } + } + + @Override + public void getDistance(final long startTime, + final long endTime, + final int limit, + @NonNull final Result callback) { + if (isInvalidContext(getContext(), callback)) return; + if (!isAvailable(callback)) return; + if (!InputKitTimeUtils.validateTimeInput(startTime, endTime, callback)) return; + + callWithValidToken(new AccessTokenListener() { + @Override + public void onSuccess() { + Options.Builder builder = new Options.Builder() + .startTime(startTime) + .endTime(endTime) + .limitation(limit <= 0 ? DataReadRequest.NO_LIMIT : limit); + // Guard the aggregation data. If limit is not specified then we need to use + // data aggregation to optimize query performance. + if (limit <= 0) builder.useDataAggregation(); + Options options = builder.build(); + mFitHistory.getDistance(options, callback); + } + + @Override + public void onFailure(Exception e) { + callback.onError(new IKResultInfo(IKStatus.Code.INVALID_REQUEST, + e.getMessage())); + } + }, SampleType.DISTANCE_WALKING_RUNNING); + } + + @Override + public void getDistanceSamples(final long startTime, + final long endTime, + final int limit, + @NonNull final Result>> callback) { + if (isInvalidContext(getContext(), callback)) return; + if (!isAvailable(callback)) return; + if (!InputKitTimeUtils.validateTimeInput(startTime, endTime, callback)) return; + + callWithValidToken(new AccessTokenListener() { + @Override + public void onSuccess() { + Options.Builder builder = new Options.Builder() + .startTime(startTime) + .endTime(endTime) + .limitation(limit <= 0 ? DataReadRequest.NO_LIMIT : limit); + // Guard the aggregation data. If limit is not specified then we need to use + // data aggregation to optimize query performance. + if (limit <= 0) builder.useDataAggregation(); + Options options = builder.build(); + mFitHistory.getDistanceSamples(options, callback); + } + + @Override + public void onFailure(Exception e) { + callback.onError(new IKResultInfo(IKStatus.Code.INVALID_REQUEST, + e.getMessage())); + } + }, SampleType.DISTANCE_WALKING_RUNNING); + } + + @Override + public void getStepCount(@NonNull final Result callback) { + Context context = getContext(); + if (isInvalidContext(context, callback)) return; + if (!isAvailable(callback)) return; + + callWithValidToken(new AccessTokenListener() { + @Override + public void onSuccess() { + mFitHistory.getStepCount(callback); + } + + @Override + public void onFailure(Exception e) { + callback.onError(new IKResultInfo(IKStatus.Code.INVALID_REQUEST, + e.getMessage())); + } + }, SampleType.STEP_COUNT); + } + + @Override + public void getStepCount(final long startTime, + final long endTime, + final int limit, + @NonNull final Result callback) { + if (isInvalidContext(getContext(), callback)) return; + if (!isAvailable(callback)) return; + if (!InputKitTimeUtils.validateTimeInput(startTime, endTime, callback)) return; + + callWithValidToken(new AccessTokenListener() { + @Override + public void onSuccess() { + Options.Builder builder = new Options.Builder() + .startTime(startTime) + .endTime(endTime) + .limitation(limit <= 0 ? DataReadRequest.NO_LIMIT : limit); + // Guard the aggregation data. If limit is not specified then we need to use + // data aggregation to optimize query performance. + if (limit <= 0) builder.useDataAggregation(); + Options options = builder.build(); + mFitHistory.getStepCount(options, callback); + } + + @Override + public void onFailure(Exception e) { + callback.onError(new IKResultInfo(IKStatus.Code.INVALID_REQUEST, + e.getMessage())); + } + }, SampleType.STEP_COUNT); + } + + @Override + public void getStepCountDistribution(final long startTime, + final long endTime, + @NonNull @Interval.IntervalName final String interval, + final int limit, + @NonNull final Result callback) { + if (isInvalidContext(getContext(), callback)) return; + if (!isAvailable(callback)) return; + if (!InputKitTimeUtils.validateTimeInput(startTime, endTime, callback)) return; + + callWithValidToken(new AccessTokenListener() { + @Override + public void onSuccess() { + TimeInterval timeInterval = new TimeInterval(interval); + Options.Builder builder = new Options.Builder() + .startTime(startTime) + .endTime(endTime) + .timeInterval(timeInterval) + .limitation(limit <= 0 ? DataReadRequest.NO_LIMIT : limit); + // Guard the aggregation data. If limit is not specified then we need to use + // data aggregation to optimize query performance. + if (limit <= 0) builder.useDataAggregation(); + Options options = builder.build(); + mFitHistory.getStepCountDistribution(options, callback); + } + + @Override + public void onFailure(Exception e) { + callback.onError(new IKResultInfo(IKStatus.Code.INVALID_REQUEST, + e.getMessage())); + } + }, SampleType.STEP_COUNT); + } + + @Override + public void getSleepAnalysisSamples(long startTime, long endTime, @NonNull Result callback) { + callback.onError(new IKResultInfo(IK_NOT_AVAILABLE, IKStatus.INPUT_KIT_SERVICE_NOT_AVAILABLE)); + } + + @Override + public void getBloodPressure(long startTime, long endTime, @NonNull Result callback) { + // TODO: Implement Google Fit API to get blood pressure data from GF + callback.onError(new IKResultInfo(IK_NOT_AVAILABLE, IKStatus.INPUT_KIT_SERVICE_NOT_AVAILABLE)); + } + + @Override + public void getWeight(long startTime, long endTime, @NonNull Result callback) { + // TODO: Implement Google Fit API to get weight data from GF + callback.onError(new IKResultInfo(IK_NOT_AVAILABLE, IKStatus.INPUT_KIT_SERVICE_NOT_AVAILABLE)); + } + + @Override + public void startMonitoring(@NonNull @SampleType.SampleName final String sensorType, + @NonNull final Pair samplingRate, + @NonNull final SensorListener listener) { + final Context context = getContext(); + if (isInvalidContext(context, listener, true)) return; + if (isSensorTypeUnavailable(sensorType, listener)) return; + + assert context != null; + callWithValidToken(new AccessTokenListener() { + @Override + public void onSuccess() { + mSensorMonitoring.registerListener(sensorType, new SensorListener() { + @Override + public void onSubscribe(@NonNull IKResultInfo info) { + if (info.getResultCode() == IKStatus.Code.VALID_REQUEST) { + HealthTrackerState.save( + context, + Constant.MONITORED_HEALTH_SENSORS, + Pair.create(sensorType, true) + ); + } + listener.onSubscribe(info); + } + + @Override + public void onReceive(@NonNull SensorDataPoint data) { + listener.onReceive(data); + } + + @Override + public void onUnsubscribe(@NonNull IKResultInfo info) { + if (info.getResultCode() == IKStatus.Code.VALID_REQUEST) { + HealthTrackerState.save( + context, + Constant.MONITORED_HEALTH_SENSORS, + Pair.create(sensorType, false) + ); + } + listener.onUnsubscribe(info); + } + }); + mSensorMonitoring.startTracking(sensorType, samplingRate); + } + + @Override + public void onFailure(Exception e) { + listener.onSubscribe(new IKResultInfo(IKStatus.Code.INVALID_REQUEST, + e.getMessage())); + } + }, sensorType); + } + + @Override + public void stopMonitoring(@NonNull @SampleType.SampleName final String sensorType, + @NonNull final SensorListener listener) { + final Context context = getContext(); + if (isInvalidContext(context, listener, false)) return; + if (isSensorTypeUnavailable(sensorType, listener)) return; + + assert context != null; + callWithValidToken(new AccessTokenListener() { + @Override + public void onSuccess() { + mSensorMonitoring.registerListener(sensorType, new SensorListener() { + @Override + public void onSubscribe(@NonNull IKResultInfo info) { + if (info.getResultCode() == IKStatus.Code.VALID_REQUEST) { + HealthTrackerState.save( + context, + Constant.MONITORED_HEALTH_SENSORS, + Pair.create(sensorType, true) + ); + } + listener.onSubscribe(info); + } + + @Override + public void onReceive(@NonNull SensorDataPoint data) { + listener.onReceive(data); + } + + @Override + public void onUnsubscribe(@NonNull IKResultInfo info) { + if (info.getResultCode() == IKStatus.Code.VALID_REQUEST) { + HealthTrackerState.save( + context, + Constant.MONITORED_HEALTH_SENSORS, + Pair.create(sensorType, false) + ); + } + listener.onUnsubscribe(info); + } + }); + mSensorMonitoring.stopTracking(sensorType); + } + + @Override + public void onFailure(Exception e) { + listener.onUnsubscribe(new IKResultInfo(IKStatus.Code.INVALID_REQUEST, + e.getMessage())); + } + }, sensorType); + } + + @Override + public void startTracking(@NonNull @SampleType.SampleName final String sensorType, + @NonNull final Pair samplingRate, + @NonNull final SensorListener listener) { + final Context context = getContext(); + if (isInvalidContext(context, listener, true)) return; + if (isSensorTypeUnavailable(sensorType, listener)) return; + if (!isAvailable()) { + listener.onSubscribe(INPUT_KIT_NOT_CONNECTED); + return; + } + + assert context != null; + callWithValidToken(new AccessTokenListener() { + @Override + public void onSuccess() { + mSensorTracking.registerListener(sensorType, new SensorListener() { + @Override + public void onSubscribe(@NonNull IKResultInfo info) { + if (info.getResultCode() == IKStatus.Code.VALID_REQUEST) { + HealthTrackerState.save( + context, + Constant.TRACKED_HEALTH_SENSORS, + Pair.create(sensorType, true) + ); + } + listener.onSubscribe(info); + } + + @Override + public void onReceive(@NonNull SensorDataPoint data) { + listener.onReceive(data); + } + + @Override + public void onUnsubscribe(@NonNull IKResultInfo info) { + if (info.getResultCode() == IKStatus.Code.VALID_REQUEST) { + HealthTrackerState.save( + context, + Constant.TRACKED_HEALTH_SENSORS, + Pair.create(sensorType, false) + ); + } + listener.onUnsubscribe(info); + } + }); + mSensorTracking.startTracking(sensorType, samplingRate); + } + + @Override + public void onFailure(Exception e) { + listener.onSubscribe(new IKResultInfo(IKStatus.Code.INVALID_REQUEST, + e.getMessage())); + } + }, sensorType); + } + + @Override + public void stopTracking(@NonNull final String sensorType, + @NonNull final SensorListener listener) { + final Context context = getContext(); + if (isInvalidContext(context, listener, false)) return; + if (isSensorTypeUnavailable(sensorType, listener)) return; + if (!isAvailable()) { + listener.onUnsubscribe(INPUT_KIT_NOT_CONNECTED); + return; + } + + assert context != null; + callWithValidToken(new AccessTokenListener() { + @Override + public void onSuccess() { + mSensorTracking.registerListener(sensorType, new SensorListener() { + @Override + public void onSubscribe(@NonNull IKResultInfo info) { + if (info.getResultCode() == IKStatus.Code.VALID_REQUEST) { + HealthTrackerState.save( + context, + Constant.TRACKED_HEALTH_SENSORS, + Pair.create(sensorType, true) + ); + } + listener.onSubscribe(info); + } + + @Override + public void onReceive(@NonNull SensorDataPoint data) { + listener.onReceive(data); + } + + @Override + public void onUnsubscribe(@NonNull IKResultInfo info) { + if (info.getResultCode() == IKStatus.Code.VALID_REQUEST) { + HealthTrackerState.save( + context, + Constant.TRACKED_HEALTH_SENSORS, + Pair.create(sensorType, false) + ); + } + listener.onUnsubscribe(info); + } + }); + mSensorTracking.stopTracking(sensorType); + } + + @Override + public void onFailure(Exception e) { + listener.onUnsubscribe(new IKResultInfo(IKStatus.Code.INVALID_REQUEST, + e.getMessage())); + } + }, sensorType); + } + + @Override + public void stopTrackingAll(@NonNull final SensorListener listener) { + final Context context = getContext(); + if (isInvalidContext(context, listener, false)) return; + if (!isAvailable()) { + listener.onUnsubscribe(INPUT_KIT_NOT_CONNECTED); + return; + } + + assert context != null; + callWithValidToken(new AccessTokenListener() { + @Override + public void onSuccess() { + mSensorTracking.stopTrackingAll(new SensorListener() { + @Override + public void onSubscribe(@NonNull IKResultInfo info) { + if (info.getResultCode() == IKStatus.Code.VALID_REQUEST) { + HealthTrackerState.saveAll( + context, + Constant.TRACKED_HEALTH_SENSORS, + true + ); + } + listener.onSubscribe(info); + } + + @Override + public void onReceive(@NonNull SensorDataPoint data) { + listener.onReceive(data); + } + + @Override + public void onUnsubscribe(@NonNull IKResultInfo info) { + if (info.getResultCode() == IKStatus.Code.VALID_REQUEST) { + HealthTrackerState.saveAll( + context, + Constant.TRACKED_HEALTH_SENSORS, + false + ); + } + listener.onSubscribe(info); + } + }); + } + + @Override + public void onFailure(Exception e) { + listener.onUnsubscribe(new IKResultInfo(IKStatus.Code.INVALID_REQUEST, + e.getMessage())); + } + }, SampleType.STEP_COUNT, SampleType.DISTANCE_WALKING_RUNNING); + } + + /** + * Helper function to check whether sensor type is available or not. + * @param sensorType Sensor type + * @param listener {@link SensorListener} + * @return True if available, False otherwise. + */ + private boolean isSensorTypeUnavailable(@NonNull String sensorType, @NonNull SensorListener listener) { + if (!SampleType.checkFitSampleType(sensorType).equals(SampleType.UNAVAILABLE)) return false; + listener.onSubscribe( + new IKResultInfo( + IKStatus.Code.INVALID_REQUEST, + sensorType + " : SENSOR_TYPE_IS_NOT_AVAILABLE!" + ) + ); + return true; + } + + /** + * Check either context is still valid or not. + * @param context Current application context. + * @param callback Result callback + * @return True if context is valid, False otherwise. + */ + private boolean isInvalidContext(@Nullable Context context, + @NonNull Result callback) { + if (context != null) return false; + + onUnreachableContext(callback); + return true; + } + + /** + * + * Check either context is still valid or not. + * @param context Current application context. + * @param listener Sensor listener. + * @param isSubscribeAction Set to true if it's coming from subscriptions, False otherwise. + * @return True if context is valid, False otherwise. + */ + private boolean isInvalidContext(@Nullable Context context, + @NonNull SensorListener listener, + boolean isSubscribeAction) { + if (context != null) return false; + + if (isSubscribeAction) listener.onSubscribe(UNREACHABLE_CONTEXT); + else listener.onUnsubscribe(UNREACHABLE_CONTEXT); + onUnreachableContext(); + return true; + } + + private void callWithValidToken(@NonNull final AccessTokenListener listener, final String... permissionTypes) { + final Context context = getContext(); + + if (context == null) { + listener.onFailure(new Exception(IKStatus.INPUT_KIT_UNREACHABLE_CONTEXT)); + return; + } + + GoogleSignInAccount account = GoogleSignIn.getLastSignedInAccount(context); + if (account != null && account.isExpired()) { + startSilentLoggedIn(context, listener, permissionTypes); + return; + } + + if (account == null) { + // If we don't have last signed in account then client should call authorize request first. + listener.onFailure(new Exception(IKStatus.INPUT_KIT_REQUIRED_GRANTED_PERMISSIONS)); + return; + } + + Log.d(TAG, "=========== @@@TOKEN IS VALID@@@ ==========="); + listener.onSuccess(); + } + + /** + * Perform Sign-in Interactively. + * This is required if user never logged-in with Google Account before. + * https://developers.google.com/games/services/android/signin#performing_interactive_sign-in + * + * @param permissionTypes Sample data type of permission that we need to ask for. + */ + private void startSignedInAndAskForPermission(String... permissionTypes) { + // Performing UI logged in only if possible. + if (getHostActivity() != null && getContext() != null) { + GoogleSignInClient signInClient = GoogleSignIn.getClient(getContext(), getOptions(permissionTypes)); + Intent intent = signInClient.getSignInIntent(); + getHostActivity().startActivityForResult(intent, GF_PERMISSION_REQUEST_CODE); + } + } + + /** + * Start silent logged in to Access Fit API in case short-lived access token invalid. + * @param context Current application context + * @param listener Access token listener + * @param permissionTypes Sample data type of permission that we need to ask for. + */ + private void startSilentLoggedIn(@NonNull Context context, + @NonNull final AccessTokenListener listener, + final String... permissionTypes) { + + GoogleSignInAccount account = GoogleSignIn.getLastSignedInAccount(context); + if (account != null) { + Log.d(TAG, "=========== !!!FITNESS ACCESS TOKEN IS INVALID!!! ==========="); + Log.d(TAG, "=========== !!!STARTING TO PERFORM SILENT LOGIN!!! ==========="); + GoogleSignIn.getClient(context, getOptions(permissionTypes)) + .silentSignIn() + .addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + if (task.isSuccessful()) { + Log.d(TAG, "=========== !!!RENEWAL ACCESS TOKEN SUCCESS!!! ==========="); + listener.onSuccess(); + } else { + Log.d(TAG, "=========== !!!RENEWAL ACCESS TOKEN FAILED!!! ==========="); + Exception err = new Exception("Unable to perform silent logged in!"); + if (task.getException() != null) { + err = task.getException(); + } + err.printStackTrace(); + listener.onFailure(err); + + // FIXME: + // If we are unable to perform silent logged in, then we have no choice + // unless we perform interactive logged in. + // BUT it also has a drawback, due to popup might appear a couple times + // each time silent logged in fail. + // startSignedInAndAskForPermission(permissionTypes); + } + } + }); + } + } + + /** + * Get default google sign in options that being used for Google Fit. + * @param permissionTypes Sample data type of permission that we need to ask for. + * @return {@link GoogleSignInOptions} + */ + private GoogleSignInOptions getOptions(String... permissionTypes) { + return new GoogleSignInOptions.Builder() + .requestId() + .requestEmail() + .addExtension(FitPermissionSet.getInstance().getPermissionsSet(permissionTypes)) + .build(); + } + + interface AccessTokenListener { + void onSuccess(); + void onFailure(Exception e); + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/history/DataNormalizer.java b/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/history/DataNormalizer.java new file mode 100644 index 0000000..66a849b --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/history/DataNormalizer.java @@ -0,0 +1,399 @@ +package nl.sense.rninputkit.inputkit.googlefit.history; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import android.util.Pair; + +import java.util.ArrayList; +import java.util.List; + +import nl.sense.rninputkit.inputkit.entity.DateContent; +import nl.sense.rninputkit.inputkit.entity.IKValue; +import nl.sense.rninputkit.inputkit.entity.TimeInterval; +import nl.sense.rninputkit.inputkit.helper.CollectionUtils; + +import static nl.sense.rninputkit.inputkit.helper.InputKitTimeUtils.getMinuteDiff; +import static nl.sense.rninputkit.inputkit.helper.InputKitTimeUtils.isOverlappingTimeWindow; +import static nl.sense.rninputkit.inputkit.helper.InputKitTimeUtils.isWithinTimeWindow; +import static nl.sense.rninputkit.inputkit.helper.InputKitTimeUtils.populateTimeWindows; + +public abstract class DataNormalizer { + + /** + * Setup input kit values according to source values. + * Next item is required to distribute source value into current and the next items + * in case it overlap both current and next time periods. + * + * @param currentItem Current item input kit value + * @param nextItem Next item input kit value + * @param sourceValues Source values + */ + protected abstract void setValueItems( + @NonNull IKValue currentItem, + @Nullable IKValue nextItem, + @NonNull List> sourceValues); + + /** + * Normalize input kit values time window. + * + * @param values Input kit values + * @param interval {@link TimeInterval} + * @return Step history within proper time windows. + */ + @NonNull + public List> normalize(long startTime, + long endTime, + @NonNull List> values, + @NonNull TimeInterval interval) { + // populate proper time windows + List> timeWindows = populateTimeWindows( + startTime, + endTime, + interval + ); + + // make sure to sort input kit values ascending + CollectionUtils.sort(true, values); + List> ikValues = populateIKValues(timeWindows); + + // setup input kit values + setupIKValues(ikValues, values); + return ikValues; + } + + /** + * Populate proper time period for input kit values. + * + * @param timeWindows Time windows + * @return Input kit values with proper time period + */ + @NonNull + private List> populateIKValues(@NonNull List> timeWindows) { + List> results = new ArrayList<>(); + for (Pair normalizedTimeWindow : timeWindows) { + results.add(new IKValue( + new DateContent(normalizedTimeWindow.first), + new DateContent(normalizedTimeWindow.second)) + ); + } + return results; + } + + /** + * Setup input kit values according to source values + * @param ikValues Input kit values within proper time period + * @param sourceValues Source values + */ + private void setupIKValues(@NonNull List> ikValues, + @NonNull List> sourceValues) { + for (int i = 0; i < ikValues.size(); i++) { + IKValue currentItem = ikValues.get(i); + IKValue nextItem = i == ikValues.size() - 1 + ? null : ikValues.get(i + 1); + setValueItems(currentItem, nextItem, sourceValues); + } + } + + /** + * Get pair of time period of current item and the next item. + * + * @param currentItem Current item input kit value + * @param nextItem Next item input kit value + * @return Pair of time period of current item and the next item + */ + private TimePeriod getPairTimePeriod( + @NonNull IKValue currentItem, + @Nullable IKValue nextItem) { + Pair currentTimePeriod = Pair.create(currentItem.getStartDate().getEpoch(), + currentItem.getEndDate().getEpoch()); + Pair nextTimePeriod = nextItem == null + ? null + : Pair.create(nextItem.getStartDate().getEpoch(), nextItem.getEndDate().getEpoch()); + return new TimePeriod(currentTimePeriod, nextTimePeriod); + } + + /** + * Set current and next item value according to source values in float data type. + * + * @param currentItem Current item input kit value + * @param nextItem Next item input kit value + * @param sourceValues Source values + */ + protected void setAsFloat( + @NonNull IKValue currentItem, + @Nullable IKValue nextItem, + @NonNull List> sourceValues) { + TimePeriod timePeriod = getPairTimePeriod(currentItem, nextItem); + + // Get pair of overlap values. + // First item will be added to current item, second value will be added to the next value. + ValueItems valueItems = getValuePair(timePeriod.currentPeriod, + timePeriod.nexTimePeriod, sourceValues); + + // Setup current value. + Float value = currentItem.getValue(); + float incomingValue = valueItems.current.floatValue(); + currentItem.setValue(value == null + ? incomingValue + : (value + incomingValue)); + + if (nextItem != null) { + currentItem.setValue(currentItem.getValue() + valueItems.floatOffset); + nextItem.setValue(valueItems.next.floatValue()); + } + } + + /** + * Set current and next item value according to source values in integer data type. + * + * @param currentItem Current item input kit value + * @param nextItem Next item input kit value + * @param sourceValues Source values + */ + protected void setAsInt( + @NonNull IKValue currentItem, + @Nullable IKValue nextItem, + @NonNull List> sourceValues) { + TimePeriod timePeriod = getPairTimePeriod(currentItem, nextItem); + + // Get pair of overlap values. + // First item will be added to current item, second value will be added to the next value. + ValueItems valueItems = getValuePair(timePeriod.currentPeriod, + timePeriod.nexTimePeriod, sourceValues); + + // Setup current value. + Integer value = currentItem.getValue(); + int incomingVal = Math.round(valueItems.current.floatValue()); + currentItem.setValue(value == null + ? incomingVal + : (value + incomingVal)); + + if (nextItem != null) { + currentItem.setValue(currentItem.getValue() + valueItems.intOffset); + nextItem.setValue(Math.round(valueItems.next.floatValue())); + } + } + + /** + * Get value for current and next value item according to source values. + * + * @param currentTimePeriod Current time period input kit value + * @param nextTimePeriod Next time period of input kit value + * @param sourceValues Source values + * @return Pair of total value source. + * - First value is a total of source values if it's completely inside time period + * of current item. + * - Second value is distributed source values if it's overlap current time period or next item + * time period as well. + */ + private ValueItems getValuePair( + @NonNull Pair currentTimePeriod, + @Nullable Pair nextTimePeriod, + @NonNull List> sourceValues) { + Number totalValue = 0, nextValue = 0, actualValue = 0; + for (IKValue value : sourceValues) { + Pair valueTimePeriod = Pair.create(value.getStartDate().getEpoch(), + value.getEndDate().getEpoch()); + + // Stop counting if value time period exceed end time of the next item time period. + if (nextTimePeriod != null && valueTimePeriod.second > nextTimePeriod.second) { + break; + } + + // Sum up current total value with source value when it still completely within time period. + if (isWithinTimeWindow(valueTimePeriod.first, valueTimePeriod.second, currentTimePeriod)) { + totalValue = sumValues(totalValue, value.getValue()); + actualValue = sumValues(actualValue, value.getValue()); + continue; + } + + // Distribute value source to current and the next item when it's overlap. + if (isOverlappingTimeWindow(valueTimePeriod.first, valueTimePeriod.second, currentTimePeriod) + && !value.isFlaggedOverlap()) { + Pair overlappingValuePair = getOverlappingValuePair(currentTimePeriod, value); + totalValue = sumValues(totalValue, overlappingValuePair.first); + actualValue = sumValues(actualValue, value.getValue()); + nextValue = overlappingValuePair.second; + value.setFlagOverlap(true); + break; + } + } + return new ValueItems(totalValue, nextValue, actualValue); + } + + /** + * Distribute value among current and the next item when source value item overlap those. + * + * @param currentTimePeriod Current time period of input kit value + * @param sourceValue Source value item + * @return Pair of overlapping value. + * First value is a total value for current item. + * Second value is an overlap value for the next item. + */ + private Pair getOverlappingValuePair( + @NonNull Pair currentTimePeriod, + @NonNull IKValue sourceValue) { + // Get specific source value overlapping item information + Pair sourceTimePeriod = Pair.create(sourceValue.getStartDate().getEpoch(), + sourceValue.getEndDate().getEpoch()); + boolean isStartWithinTimePeriod = isWithinTimeWindow(sourceTimePeriod.first, currentTimePeriod); + boolean isEndWithinTimePeriod = isWithinTimeWindow(sourceTimePeriod.second, currentTimePeriod); + + // It means : Source value end time exceed an end time of current time period + // In this case, we distribute `right`-extra-value to the next input kit item + // + // eg. + // - current time period : 08.00 - 08.10 + // - source time period : 08.00 - 08.11 + if (isStartWithinTimePeriod && !isEndWithinTimePeriod + && sourceTimePeriod.second >= currentTimePeriod.second) { + return getValuePair(sourceValue, currentTimePeriod.second, sourceTimePeriod); + } + + // It means : Source value `start-time` was below of `start-time` of the current time period + // In this case, we exclude `left`-extra-value and calculate average value within time period + // to the current input kit item + // + // eg. + // - current time period : 08.00 - 08.10 + // - source time period : 07.58 - 08.08 + if (!isStartWithinTimePeriod && sourceTimePeriod.first < currentTimePeriod.first + && isEndWithinTimePeriod) { + Pair valuePair = getValuePair(sourceValue, + currentTimePeriod.first, sourceTimePeriod); + return Pair.create(valuePair.second, 0f); + } + + // It means : Time period was completely within source value `time-window`. In this case, + // we only calculate value for intersects `time-window` of source value and current time + // period. Then those calculated value will be distributed to the current input kit item. + // + // eg. + // - current time period : 08.00 - 08.10 + // - source time period : 07.58 - 08.18 + if (!isStartWithinTimePeriod && sourceTimePeriod.first < currentTimePeriod.first + && !isEndWithinTimePeriod && sourceTimePeriod.second >= currentTimePeriod.second) { + float srcAvgPerMinute = averageValuePerMinute(sourceValue); + long timePeriodMinDiff = getMinuteDiff(currentTimePeriod.second, currentTimePeriod.first); + return Pair.create(srcAvgPerMinute * timePeriodMinDiff, 0f); + } + + return Pair.create(0f, 0f); + } + + /** + * Get left-right step count value distribution per minute. + * @param sourceValue Source value + * @param anchorTime Anchor time + * @param sourceTimePeriod Source time period + * @return Pair of left and right overlap value. + */ + private Pair getValuePair( + @NonNull IKValue sourceValue, + long anchorTime, + @NonNull Pair sourceTimePeriod) { + float srcAvgPerMinute = averageValuePerMinute(sourceValue); + long leftMinDiff = getMinuteDiff(sourceTimePeriod.first, anchorTime); + long rightMinDiff = getMinuteDiff(anchorTime, sourceTimePeriod.second); + if (leftMinDiff == 0 && rightMinDiff == 0) { + // In this case, source time period overlap anchor time within milliseconds. + // Then distribute average value into current item of input kit value. + return Pair.create(srcAvgPerMinute, 0f); + } + return Pair.create(srcAvgPerMinute * leftMinDiff, srcAvgPerMinute * rightMinDiff); + } + + /** + * Sum values in a number data type + * + * @param previous Previous value + * @param current Current value + * @return Sum of previous and current value if data type recognised. + * Otherwise, previous value will be returned. + */ + private Number sumValues(X previous, Number current) { + if (current instanceof Long) { + return previous.longValue() + current.longValue(); + } + if (current instanceof Float) { + return previous.floatValue() + current.floatValue(); + } + if (current instanceof Integer) { + return previous.intValue() + current.intValue(); + } + if (current instanceof Double) { + return previous.doubleValue() + current.doubleValue(); + } + if (current instanceof Short) { + return previous.shortValue() + current.shortValue(); + } + if (current instanceof Byte) { + return previous.byteValue() + current.byteValue(); + } + return previous; + } + + /** + * Average value per minutes + * + * @param sourceValue Source value + * @return Average value per minute if data type recognised. + * Otherwise, default value (1L) will be returned. + */ + private float averageValuePerMinute(IKValue sourceValue) { + float minDiff = getMinuteDiff(sourceValue.getEndDate().getEpoch(), + sourceValue.getStartDate().getEpoch()); + minDiff = minDiff == 0f ? 1f : minDiff; + Number value = sourceValue.getValue(); + if (value instanceof Long) { + return value.longValue() / minDiff; + } + if (value instanceof Float) { + return value.floatValue() / minDiff; + } + if (value instanceof Integer) { + return value.intValue() / minDiff; + } + if (value instanceof Double) { + return (float) value.doubleValue() / minDiff; + } + if (value instanceof Short) { + return value.shortValue() / minDiff; + } + if (value instanceof Byte) { + return value.byteValue() / minDiff; + } + return 1f; + } + + class ValueItems { + private Number current; + private Number next; + private Number actual; + private int intOffset = 0; + private float floatOffset = 0.f; + + ValueItems(Number current, Number next, Number actual) { + this.current = current; + this.next = next; + this.actual = actual; + calculateOffset(); + } + + private void calculateOffset() { + this.intOffset = actual.intValue() + - (Math.round(current.floatValue()) + Math.round(next.floatValue())); + this.floatOffset = actual.floatValue() - (current.floatValue() + next.floatValue()); + } + } + + class TimePeriod { + private Pair currentPeriod; + private Pair nexTimePeriod; + + TimePeriod(Pair currentPeriod, Pair nexTimePeriod) { + this.currentPeriod = currentPeriod; + this.nexTimePeriod = nexTimePeriod; + } + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/history/DistanceHistoryTask.java b/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/history/DistanceHistoryTask.java new file mode 100644 index 0000000..c817fcb --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/history/DistanceHistoryTask.java @@ -0,0 +1,145 @@ +package nl.sense.rninputkit.inputkit.googlefit.history; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import android.util.Pair; + +import com.google.android.gms.fitness.data.DataPoint; +import com.google.android.gms.fitness.data.DataType; +import com.google.android.gms.fitness.result.DataReadResponse; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import nl.sense.rninputkit.inputkit.Options; +import nl.sense.rninputkit.inputkit.entity.IKValue; + +class DistanceHistoryTask extends HistoryTaskFactory { + private DataNormalizer normalizer = new DataNormalizer() { + @NonNull + @Override + protected void setValueItems(@NonNull IKValue currentItem, + @Nullable IKValue nextItem, + @NonNull List> sourceValues) { + this.setAsFloat(currentItem, nextItem, sourceValues); + } + }; + + private HistoryExtractor extractor = new HistoryExtractor() { + @Override + protected Float getDataPointValue(@Nullable DataPoint dataPoint) { + return this.asFloat(dataPoint); + } + }; + + private DistanceHistoryTask(IFitReader fitDataReader, + List> safeRequests, + Options options, + DataType dataTypeRequest, + Pair aggregateType, + OnCompleteListener onCompleteListener, + OnFailureListener onFailureListener) { + super(fitDataReader, + safeRequests, + options, + dataTypeRequest, + aggregateType, + onCompleteListener, + onFailureListener + ); + } + + @Override + protected List> getValues(List responses) { + if (responses == null) return Collections.emptyList(); + + List> fitValues = new ArrayList<>(); + for (DataReadResponse response : responses) { + if (!response.getStatus().isSuccess()) continue; + + // extract value history + List> values = extractor.extractHistory(response, options.isUseDataAggregation()); + + // check data source availability + if (values.isEmpty()) continue; + + fitValues.addAll(values); + } + + // normalise time window + return normalizer.normalize(options.getStartTime(), + options.getEndTime(), fitValues, options.getTimeInterval()); + } + + static class Builder { + private IFitReader fitDataReader; + private List> safeRequests; + private Options options; + private DataType dataTypeRequest; + private Pair aggregateType; + private OnCompleteListener onCompleteListener; + private OnFailureListener onFailureListener; + + Builder withFitDataReader(IFitReader fitDataReader) { + this.fitDataReader = fitDataReader; + return this; + } + + Builder addSafeRequests(List> safeRequests) { + this.safeRequests = safeRequests; + return this; + } + + Builder addOptions(Options options) { + this.options = options; + return this; + } + + Builder addDataType(DataType dataTypeRequest) { + this.dataTypeRequest = dataTypeRequest; + return this; + } + + Builder addAggregateTypes(Pair aggregateType) { + this.aggregateType = aggregateType; + return this; + } + + Builder addOnCompleteListener(OnCompleteListener onCompleteListener) { + this.onCompleteListener = onCompleteListener; + return this; + } + + Builder addOnFailureListener(OnFailureListener onFailureListener) { + this.onFailureListener = onFailureListener; + return this; + } + + private void validate() { + if (fitDataReader == null) + throw new IllegalStateException("Fit history must be provided."); + if (safeRequests == null) + throw new IllegalStateException("Time requests must be provided."); + if (dataTypeRequest == null) + throw new IllegalStateException("Data type request must be provided."); + if (aggregateType == null) + throw new IllegalStateException("Aggregate type must be provided."); + if (options == null) + throw new IllegalStateException("Options history must be provided."); + } + + DistanceHistoryTask build() { + validate(); + return new DistanceHistoryTask( + fitDataReader, + safeRequests, + options, + dataTypeRequest, + aggregateType, + onCompleteListener, + onFailureListener + ); + } + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/history/FitHistory.java b/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/history/FitHistory.java new file mode 100644 index 0000000..c5d083a --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/history/FitHistory.java @@ -0,0 +1,285 @@ +package nl.sense.rninputkit.inputkit.googlefit.history; + +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import android.util.Pair; + +import com.google.android.gms.auth.api.signin.GoogleSignIn; +import com.google.android.gms.fitness.Fitness; +import com.google.android.gms.fitness.data.DataPoint; +import com.google.android.gms.fitness.data.DataSet; +import com.google.android.gms.fitness.data.DataSource; +import com.google.android.gms.fitness.data.DataType; +import com.google.android.gms.fitness.request.DataReadRequest; +import com.google.android.gms.fitness.result.DataReadResponse; +import com.google.android.gms.tasks.OnFailureListener; +import com.google.android.gms.tasks.OnSuccessListener; +import com.google.android.gms.tasks.Task; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import nl.sense.rninputkit.inputkit.InputKit.Result; +import nl.sense.rninputkit.inputkit.Options; +import nl.sense.rninputkit.inputkit.constant.IKStatus; +import nl.sense.rninputkit.inputkit.entity.IKValue; +import nl.sense.rninputkit.inputkit.entity.StepContent; +import nl.sense.rninputkit.inputkit.entity.TimeInterval; +import nl.sense.rninputkit.inputkit.status.IKResultInfo; + +/** + * Created by panjiyudasetya on 6/15/17. + */ + +@SuppressWarnings("SpellCheckingInspection") +public class FitHistory implements IFitReader { + private Context mContext; + private SafeRequestHandler mSafeRequestHandler; + + public FitHistory(@NonNull Context context) { + this.mContext = context; + this.mSafeRequestHandler = new SafeRequestHandler(); + } + + /** + * Get total distance of walk within specific options. + * + * @param options {@link Options} + * @param callback {@link Result} containing number of total distance + */ + public void getDistance(@NonNull final Options options, + @NonNull final Result callback) { + // Invoke the History API to fetch the data with the query and await the result of + // the read request. + List> safeRequests = mSafeRequestHandler.getSafeRequest(options.getStartTime(), + options.getEndTime(), options.getTimeInterval()); + new DistanceHistoryTask.Builder() + .withFitDataReader(this) + .addSafeRequests(safeRequests) + .addOptions(options) + .addDataType(DataType.TYPE_DISTANCE_DELTA) + .addAggregateTypes(Pair.create(DataType.TYPE_DISTANCE_DELTA, DataType.AGGREGATE_DISTANCE_DELTA)) + .addOnCompleteListener(new HistoryTaskFactory.OnCompleteListener() { + @Override + public void onComplete(List> result) { + callback.onNewData(IKValue.getTotalFloats(result)); + } + }) + .addOnFailureListener(new HistoryTaskFactory.OnFailureListener() { + @Override + public void onFailure(List exceptions) { + callback.onError(new IKResultInfo(IKStatus.Code.INVALID_REQUEST, + exceptions.get(0).getMessage())); + } + }) + .build() + .start(); + } + + /** + * Get sample distance of walk within specific options. + * + * @param options {@link Options} + * @param callback {@link Result} containing number of total distance + */ + public void getDistanceSamples(@NonNull final Options options, + @NonNull final Result>> callback) { + // Invoke the History API to fetch the data with the query and await the result of + // the read request. + List> safeRequests = mSafeRequestHandler.getSafeRequest(options.getStartTime(), + options.getEndTime(), options.getTimeInterval()); + new DistanceHistoryTask.Builder() + .withFitDataReader(this) + .addSafeRequests(safeRequests) + .addOptions(options) + .addDataType(DataType.TYPE_DISTANCE_DELTA) + .addAggregateTypes(Pair.create(DataType.TYPE_DISTANCE_DELTA, DataType.AGGREGATE_DISTANCE_DELTA)) + .addOnCompleteListener(new HistoryTaskFactory.OnCompleteListener() { + @Override + public void onComplete(List> result) { + callback.onNewData(applyLimitation(options.getLimitation(), result)); + } + }) + .addOnFailureListener(new HistoryTaskFactory.OnFailureListener() { + @Override + public void onFailure(List exceptions) { + callback.onError(new IKResultInfo(IKStatus.Code.INVALID_REQUEST, + exceptions.get(0).getMessage())); + } + }) + .build() + .start(); + } + + /** + * Get daily total step count. + * + * @param callback {@link Result} containing number of total steps count + */ + public void getStepCount(@NonNull final Result callback) { + Fitness.getHistoryClient(mContext, GoogleSignIn.getLastSignedInAccount(mContext)) + .readDailyTotal(DataType.TYPE_STEP_COUNT_DELTA) + .addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(DataSet dataSet) { + List> contents = new HistoryExtractor() { + @Override + protected Integer getDataPointValue(@Nullable DataPoint dataPoint) { + return this.asInt(dataPoint); + } + }.historyFromDataSet(dataSet); + callback.onNewData(IKValue.getTotalIntegers(contents)); + } + }) + .addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(@NonNull Exception e) { + callback.onError(new IKResultInfo(IKStatus.Code.INVALID_REQUEST, + e.getMessage())); + } + }); + } + + /** + * Get total steps count of specific range + * + * @param options Steps count options + * @param callback {@link Result } containing number of total steps count + */ + @SuppressWarnings("unused")//This is a public API + public void getStepCount(@NonNull final Options options, + @NonNull final Result callback) { + // Invoke the History API to fetch the data with the query and await the result of + // the read request. + List> safeRequests = mSafeRequestHandler.getSafeRequest(options.getStartTime(), + options.getEndTime(), options.getTimeInterval()); + new StepCountHistoryTask.Builder() + .withFitDataReader(this) + .addSafeRequests(safeRequests) + .addOptions(options) + .addDataType(DataType.TYPE_STEP_COUNT_DELTA) + .addAggregateSourceType(Pair.create(getFitStepCountDataSource(), DataType.AGGREGATE_STEP_COUNT_DELTA)) + .addOnCompleteListener(new HistoryTaskFactory.OnCompleteListener() { + @Override + public void onComplete(List> result) { + callback.onNewData(IKValue.getTotalIntegers(result)); + } + }) + .addOnFailureListener(new HistoryTaskFactory.OnFailureListener() { + @Override + public void onFailure(List exceptions) { + callback.onError(new IKResultInfo(IKStatus.Code.INVALID_REQUEST, + exceptions.get(0).getMessage())); + } + }) + .build() + .start(); + } + + /** + * Get distribution step count history by specific time period. + * This function should be called within asynchronous process because of + * reading historical data through {@link Fitness#HistoryApi} will be executed on main + * thread by default. + * + * @param options Steps count options + * @param callback {@link Result} containing a set of step content + */ + @SuppressWarnings("unused")//This is a public API + public void getStepCountDistribution(@NonNull final Options options, + @NonNull final Result callback) { + // Invoke the History API to fetch the data with the query and await the result of + // the read request. + List> safeRequests = mSafeRequestHandler.getSafeRequest(options.getStartTime(), + options.getEndTime(), options.getTimeInterval()); + new StepCountHistoryTask.Builder() + .withFitDataReader(this) + .addSafeRequests(safeRequests) + .addOptions(options) + .addDataType(DataType.TYPE_STEP_COUNT_DELTA) + .addAggregateSourceType(Pair.create(getFitStepCountDataSource(), DataType.AGGREGATE_STEP_COUNT_DELTA)) + .addOnCompleteListener(new HistoryTaskFactory.OnCompleteListener() { + @Override + public void onComplete(List> result) { + StepContent content = StepCountHistoryTask.toStepContent( + applyLimitation(options.getLimitation(), result), + options.getStartTime(), options.getEndTime()); + callback.onNewData(content); + } + }) + .addOnFailureListener(new HistoryTaskFactory.OnFailureListener() { + @Override + public void onFailure(List exceptions) { + callback.onError(new IKResultInfo(IKStatus.Code.INVALID_REQUEST, + exceptions.get(0).getMessage())); + } + }) + .build() + .start(); + } + + /** + * To make sure that returned step count data exactly the same with GoogleFit App + * we need to define Google Fit data source + * @return Google Fit datasource + */ + private DataSource getFitStepCountDataSource() { + return new DataSource.Builder() + .setDataType(DataType.TYPE_STEP_COUNT_DELTA) + .setType(DataSource.TYPE_DERIVED) + .setStreamName("estimated_steps") + .setAppPackageName("com.google.android.gms") + .build(); + } + + @Override + public synchronized Task readHistory(long startTime, + long endTime, + boolean useDataAggregation, + @NonNull TimeInterval timeIntervalAggregator, + DataType fitDataType, + Pair typeAggregator) { + DataReadRequest.Builder requestBuilder = new DataReadRequest.Builder(); + if (useDataAggregation) { + // The data request can specify multiple data types to return, effectively + // combining multiple data queries into one call. + // In this example, it's very unlikely that the request is for several hundred + // data points each consisting of cumulative distance in meters and a timestamp. + // The more likely scenario is wanting to see how many distance were achieved + // per day, for several days. + if (DataSource.class.isInstance(typeAggregator.first)) { + requestBuilder.aggregate((DataSource) typeAggregator.first, typeAggregator.second); + } else if (DataType.class.isInstance(typeAggregator.first)) { + requestBuilder.aggregate((DataType) typeAggregator.first, typeAggregator.second); + } else { + throw new IllegalStateException("Unsupported aggregate type"); + } + // Analogous to a "Group By" in SQL, defines how data should be aggregated. + // bucketByTime allows for a time span, whereas bucketBySession would allow + // bucketing by "sessions", which would need to be defined in code. + requestBuilder.bucketByTime(timeIntervalAggregator.getValue(), timeIntervalAggregator.getTimeUnit()); + } else requestBuilder.read(fitDataType); + + DataReadRequest request = requestBuilder + .setTimeRange(startTime, endTime, TimeUnit.MILLISECONDS) + .enableServerQueries() + .build(); + + return Fitness.getHistoryClient(mContext, GoogleSignIn.getLastSignedInAccount(mContext)) + .readData(request); + } + + /** + * Helper function to apply limitation from Client + * @param limit Data limitation + * @param data Current data result + * @param Data type + * @return Limited data set + */ + private List applyLimitation(Integer limit, List data) { + if (limit == null || limit <= 0 || limit > data.size()) return data; + data.subList(limit, data.size()).clear(); + return data; + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/history/HistoryExtractor.java b/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/history/HistoryExtractor.java new file mode 100644 index 0000000..5f2cf26 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/history/HistoryExtractor.java @@ -0,0 +1,167 @@ +package nl.sense.rninputkit.inputkit.googlefit.history; + +import androidx.annotation.Nullable; +import android.util.Log; + +import com.google.android.gms.fitness.data.Bucket; +import com.google.android.gms.fitness.data.DataPoint; +import com.google.android.gms.fitness.data.DataSet; +import com.google.android.gms.fitness.data.Field; +import com.google.android.gms.fitness.data.Value; +import com.google.android.gms.fitness.result.DataReadResponse; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import nl.sense.rninputkit.inputkit.entity.DateContent; +import nl.sense.rninputkit.inputkit.entity.IKValue; + +/** + * Abstraction historical data extractor from Google Fitness API + * @param Expected output type + * + * Created by panjiyudasetya on 7/5/17. + */ + +public abstract class HistoryExtractor { + private static final String TAG = "HistoryExtractor"; + + /** + * Helper function to extract historical data based on {@link DataReadResponse} and aggregation + * key + * @param dataReadResponse {@link DataReadResponse} history + * @param useDataAggregation Set true to aggregate existing data by a bucket of time periods + * @return {@link List>} Input kit values + */ + public List> extractHistory(DataReadResponse dataReadResponse, boolean useDataAggregation) { + if (useDataAggregation) { + return historyFromBucket(dataReadResponse.getBuckets()); + } else { + return historyFromDataSet(dataReadResponse.getDataSets()); + } + } + + /** + * Helper function to extract data points history from {@link DataSet} + * @param dataSet {@link DataSet} + * @return {@link List} Input kit values + */ + public List> historyFromDataSet(@Nullable DataSet dataSet) { + if (dataSet == null) return Collections.emptyList(); + Log.i(TAG, "Data returned for Data type: " + dataSet.getDataType().getName()); + + List> contents = new ArrayList<>(); + + for (DataPoint dp : dataSet.getDataPoints()) { + contents.add(new IKValue<>( + getDataPointValue(dp), + new DateContent(dp.getStartTime(TimeUnit.MILLISECONDS)), + new DateContent(dp.getEndTime(TimeUnit.MILLISECONDS)) + )); + } + return contents; + } + + /** + * Helper function to extract historical from {@link Bucket} + * @param buckets {@link List} + * @return {@link List>} Input kit values + */ + private List> historyFromBucket(@Nullable List buckets) { + if (buckets == null || buckets.isEmpty()) return Collections.emptyList(); + + List> contents = new ArrayList<>(); + int startFormIndex = 0; + for (Bucket bucket : buckets) { + List dataSets = bucket.getDataSets(); + contents.addAll(startFormIndex, historyFromDataSet(dataSets)); + startFormIndex = contents.size(); + } + return contents; + } + + /** + * Helper function to extract data point history from {@link DataSet} + * @param dataSets {@link DataSet} + * @return {@link List>} Input kit values + */ + private List> historyFromDataSet(@Nullable List dataSets) { + if (dataSets == null || dataSets.isEmpty()) return Collections.emptyList(); + + List> contents = new ArrayList<>(); + int startFormIndex = 0; + for (DataSet dataSet : dataSets) { + contents.addAll(startFormIndex, historyFromDataSet(dataSet)); + startFormIndex = contents.size(); + } + + return contents; + } + + /** + * Get data point value. + * @param dataPoint Detected value in {@link DataPoint} + * @return T value with specific type + */ + protected abstract T getDataPointValue(@Nullable DataPoint dataPoint); + + /** + * Convert data point as float value + * @param dataPoint Detected {@link DataPoint} from Fit history + * @return Float of data point value, otherwise 0.f will be returned. + * @throws {@link IllegalStateException} when `value.getFormat()` not equals a float + * -> 1 means data point value in integer format + * -> 2 means data point value in float format + * -> 3 means data point value in string format + */ + public float asFloat(@Nullable DataPoint dataPoint) { + Value value = getValue(dataPoint); + return value == null ? 0.f : value.asFloat(); + } + + /** + * Convert data point as integer value + * @param dataPoint Detected {@link DataPoint} from Fit history + * @return Integer of data point value, otherwise 0 will be returned. + * @throws {@link IllegalStateException} when `value.getFormat()` not equals integer + * -> 1 means data point value in integer format + * -> 2 means data point value in float format + * -> 3 means data point value in string format + */ + public int asInt(@Nullable DataPoint dataPoint) { + Value value = getValue(dataPoint); + return value == null ? 0 : value.asInt(); + } + + /** + * Convert data point as string value + * @param dataPoint Detected {@link DataPoint} from Fit history + * @return String of data point value, otherwise 0 will be returned. + * @throws {@link IllegalStateException} when `value.getFormat()` not equals string + * -> 1 means data point value in integer format + * -> 2 means data point value in float format + * -> 3 means data point value in string format + */ + public String asString(@Nullable DataPoint dataPoint) { + Value value = getValue(dataPoint); + return value == null ? "" : value.asString(); + } + + /** + * Get data point value. + * @param dataPoint Detected {@link DataPoint} + * @return {@link Value} of detected data point + */ + @Nullable + private Value getValue(@Nullable DataPoint dataPoint) { + if (dataPoint == null || dataPoint.getDataType() == null) return null; + + List fields = dataPoint.getDataType().getFields(); + if (fields == null || fields.isEmpty()) return null; + + // Usually this fields only contains one row, so we can directly return the value + return dataPoint.getValue(fields.get(0)); + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/history/HistoryTaskFactory.java b/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/history/HistoryTaskFactory.java new file mode 100644 index 0000000..3dca16a --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/history/HistoryTaskFactory.java @@ -0,0 +1,139 @@ +package nl.sense.rninputkit.inputkit.googlefit.history; + +import android.os.AsyncTask; +import android.util.Pair; + +import com.google.android.gms.fitness.data.DataType; +import com.google.android.gms.fitness.result.DataReadResponse; +import com.google.android.gms.tasks.Tasks; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import nl.sense.rninputkit.inputkit.Options; +import nl.sense.rninputkit.inputkit.constant.Interval; +import nl.sense.rninputkit.inputkit.entity.IKValue; +import nl.sense.rninputkit.inputkit.entity.TimeInterval; + +public abstract class HistoryTaskFactory extends AsyncTask>> { + public interface OnCompleteListener { + void onComplete(List> result); + } + public interface OnFailureListener { + void onFailure(List exceptions); + } + + private IFitReader fitDataReader; + private List> safeRequests; + private DataType dataTypeRequest; + private Pair aggregateType; + private OnCompleteListener onCompleteListener; + private OnFailureListener onFailureListener; + private HistoryResponseSet responseSet; + protected Options options; + + protected HistoryTaskFactory(IFitReader fitDataReader, + List> safeRequests, + Options options, + DataType dataTypeRequest, + Pair aggregateType, + OnCompleteListener onCompleteListener, + OnFailureListener onFailureListener) { + this.fitDataReader = fitDataReader; + this.safeRequests = safeRequests; + this.options = options; + this.dataTypeRequest = dataTypeRequest; + this.aggregateType = aggregateType; + this.onCompleteListener = onCompleteListener; + this.onFailureListener = onFailureListener; + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + responseSet = new HistoryResponseSet(); + } + + @Override + protected List> doInBackground(Void... aVoid) { + for (Pair request : safeRequests) { + try { + TimeInterval intervalAggregator = options.getTimeInterval() + .getTimeUnit() == TimeUnit.DAYS + ? new TimeInterval(Interval.ONE_DAY) + : options.getTimeInterval(); + Pair timeout = intervalAggregator + .getTimeUnit() == TimeUnit.DAYS + ? Pair.create(150, TimeUnit.SECONDS) + : Pair.create(1, TimeUnit.MINUTES); + DataReadResponse response = Tasks.await( + fitDataReader.readHistory( + request.first, + request.second, + options.isUseDataAggregation(), + intervalAggregator, + dataTypeRequest, + aggregateType + ), timeout.first, timeout.second); + responseSet.addResponse(response); + } catch (ExecutionException | InterruptedException | TimeoutException e) { + responseSet.addException(e); + } + } + + return getValues(responseSet.responses()); + } + + @Override + protected void onPostExecute(List> results) { + if (!responseSet.responses().isEmpty() || responseSet.exceptions().isEmpty()) { + if (onCompleteListener != null) onCompleteListener.onComplete(results); + return; + } + + if (onFailureListener != null) onFailureListener.onFailure(responseSet.exceptions()); + } + + /** + * Get mapped input kit values from data response + * @param responses Collection of {@link DataReadResponse} if distance sample + * @return List of distance sample + */ + protected abstract List> getValues(List responses); + + /** + * Execute task history within thread pool executor + */ + public void start() { + executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + class HistoryResponseSet { + private List responses; + private List exceptions; + + HistoryResponseSet() { + this.responses = new ArrayList<>(); + this.exceptions = new ArrayList<>(); + } + + void addResponse(DataReadResponse response) { + responses.add(response); + } + + void addException(Exception exception) { + exceptions.add(exception); + } + + List responses() { + return responses; + } + + List exceptions() { + return exceptions; + } + } +} \ No newline at end of file diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/history/IFitReader.java b/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/history/IFitReader.java new file mode 100644 index 0000000..72638f7 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/history/IFitReader.java @@ -0,0 +1,31 @@ +package nl.sense.rninputkit.inputkit.googlefit.history; + +import androidx.annotation.NonNull; +import android.util.Pair; + +import com.google.android.gms.fitness.data.DataType; +import com.google.android.gms.fitness.result.DataReadResponse; +import com.google.android.gms.tasks.Task; + +import nl.sense.rninputkit.inputkit.entity.TimeInterval; + +public interface IFitReader { + /** + * Read historical data from Fitness API. + * + * @param startTime Start time cumulative distance + * @param endTime End time cumulative distance + * @param useDataAggregation Set true to aggregate existing data by a bucket of time periods + * @param timeIntervalAggregator Time Interval for data aggregation + * @param fitDataType Fitness data type + * @param typeAggregator Pair of aggregator data type. + * First value must be source of aggregate. eg. + * Second value must be aggregate value. + */ + Task readHistory(long startTime, + long endTime, + boolean useDataAggregation, + @NonNull TimeInterval timeIntervalAggregator, + DataType fitDataType, + Pair typeAggregator); +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/history/SafeRequestHandler.java b/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/history/SafeRequestHandler.java new file mode 100644 index 0000000..b9819ed --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/history/SafeRequestHandler.java @@ -0,0 +1,137 @@ +package nl.sense.rninputkit.inputkit.googlefit.history; + +import androidx.annotation.NonNull; +import android.util.Pair; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import nl.sense.rninputkit.inputkit.entity.TimeInterval; + +public class SafeRequestHandler { + /** + * As for minutely request, we should use safe number for maximum datapoints within those + * period. + * eg.: + * - 24 hours * 1 minute datapoints = 1440 datapoint <- this one would be pretty rare. consider + * no one would keep walking or running + * for entire 24 hours without a rest + * - 24 hours * 10 minute datapoints = 144 datapoint + * - 24 hours * 30 minute datapoints = 48 datapoint + */ + private static final int SAFE_HOURS_NUMBER_FOR_MINUTELY = 24; + /** + * As for hourly / daily / weekly interval, we should use maximum datapoints for those period + * eg.: + * - max minutely : when `minuteValue` == 1 minute-interval -> 12 HOURS + * when `minuteValue` > 1 minute-interval -> 24 HOURS + * - max hourly : 1000 hours = 1000 datapoint within hourly period + * - max daily : 1000 days = 1000 datapoint within the day period + * - max weekly : 1000 days = 1000 datapoint within the day period + * + * 1000 days = 1000 datapoint for daily basis time interval + */ + private static final int SAFE_HOURS_NUMBER_FOR_HOURLY = 1000; + private static final int SAFE_DAYS_NUMBER_FOR_DAILY = 1000; + + + /** + * Get safe request of requested start and end date. + * This is required to avoid 1000++ datapoints error. Through this way, we will create another + * request chunk per 12 hours with an asumptions that : + * 12 hours * 1 minute datapoints = 720 datapoint + * @param startDate Date of start time request + * @param endDate Date of end time request + * @param timeInterval {@link TimeInterval} that specified by client + * @return List of pair of start and end time + */ + public List> getSafeRequest(long startDate, long endDate, TimeInterval timeInterval) { + long diffMillis = endDate - startDate; + final long safeHours = getSafeHours(timeInterval); + List> request = new ArrayList<>(); + if (diffMillis <= TimeUnit.HOURS.toMillis(safeHours)) { + request.add(Pair.create(startDate, endDate)); + return request; + } + + long start = startDate; + Pair valuePairCalAddition = getValuePairCalAddition(timeInterval); + + while (start < endDate) { + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(start); + cal.add(valuePairCalAddition.first, valuePairCalAddition.second); + + long relativeEndTime = cal.getTimeInMillis(); + long spanRelStartTime = getStartOfDay(relativeEndTime); + + if (relativeEndTime > spanRelStartTime) { + relativeEndTime = spanRelStartTime; + } + + if (relativeEndTime > endDate) relativeEndTime = endDate; + + request.add(Pair.create(start, relativeEndTime)); + start = relativeEndTime; + } + return request; + } + + /** + * Get safe hours for a given {@link TimeInterval} + * @param timeInterval {@link TimeInterval} + * @return Total hours + */ + private long getSafeHours(@NonNull TimeInterval timeInterval) { + TimeUnit timeUnit = timeInterval.getTimeUnit(); + final int safeNumber = getValuePairCalAddition(timeInterval).second; + if (timeUnit == TimeUnit.DAYS) { + return TimeUnit.DAYS.toHours(safeNumber); + } + return safeNumber; + } + + /** + * Get pair of value addition for calendar within available safe number. + * @param timeInterval Given time interval + * @return Pair of calendar data field and addition value + */ + private Pair getValuePairCalAddition(TimeInterval timeInterval) { + if (timeInterval.getTimeUnit() == TimeUnit.DAYS) { + return Pair.create(Calendar.DATE, SAFE_DAYS_NUMBER_FOR_DAILY); + } + if (timeInterval.getTimeUnit() == TimeUnit.HOURS) { + return Pair.create(Calendar.HOUR_OF_DAY, SAFE_HOURS_NUMBER_FOR_HOURLY); + } + if (timeInterval.getTimeUnit() == TimeUnit.MINUTES) { + switch (timeInterval.getValue()) { + case 1 : // 1 minute interval + case 10 : // 10 minute interval + case 30 : // 30 minute interval + default : return Pair.create(Calendar.HOUR_OF_DAY, SAFE_HOURS_NUMBER_FOR_MINUTELY); + } + } + return Pair.create(Calendar.HOUR_OF_DAY, SAFE_HOURS_NUMBER_FOR_MINUTELY); + } + + /** + * Get time stamp of begining of the day of the given anchor time. + * eg.: + * when `anchorTime` = '2018-10-01 00:07:00' -> `beginingOfDay` = '2018-10-01 00:00:00' + * when `anchorTime` = '2018-10-01 23:00:12' -> `beginingOfDay` = '2018-10-01 00:00:00' + * and so on + * @param anchorTime anchor time + * @return Time of end of day of the anchor time. + */ + private long getStartOfDay(long anchorTime) { + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(anchorTime); + cal.set(Calendar.HOUR_OF_DAY, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 0); + return cal.getTimeInMillis(); + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/history/StepCountHistoryTask.java b/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/history/StepCountHistoryTask.java new file mode 100644 index 0000000..99cdc6c --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/history/StepCountHistoryTask.java @@ -0,0 +1,172 @@ +package nl.sense.rninputkit.inputkit.googlefit.history; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import android.util.Pair; + +import com.google.android.gms.fitness.data.DataPoint; +import com.google.android.gms.fitness.data.DataSource; +import com.google.android.gms.fitness.data.DataType; +import com.google.android.gms.fitness.result.DataReadResponse; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import nl.sense.rninputkit.inputkit.Options; +import nl.sense.rninputkit.inputkit.entity.IKValue; +import nl.sense.rninputkit.inputkit.entity.Step; +import nl.sense.rninputkit.inputkit.entity.StepContent; + +class StepCountHistoryTask extends HistoryTaskFactory { + private DataNormalizer normalizer = new DataNormalizer() { + @NonNull + @Override + protected void setValueItems(@NonNull IKValue currentItem, + @Nullable IKValue nextItem, + @NonNull List> sourceValues) { + this.setAsInt(currentItem, nextItem, sourceValues); + } + }; + + private HistoryExtractor extractor = new HistoryExtractor() { + @Override + protected Integer getDataPointValue(@Nullable DataPoint dataPoint) { + return this.asInt(dataPoint); + } + }; + + private StepCountHistoryTask(IFitReader fitDataReader, + List> safeRequests, + Options options, + DataType dataTypeRequest, + Pair aggregateType, + OnCompleteListener onCompleteListener, + OnFailureListener onFailureListener) { + super(fitDataReader, + safeRequests, + options, + dataTypeRequest, + aggregateType, + onCompleteListener, + onFailureListener + ); + } + + @Override + protected List> getValues(List responses) { + if (responses == null) return Collections.emptyList(); + + List> fitValues = new ArrayList<>(); + for (DataReadResponse response : responses) { + if (!response.getStatus().isSuccess()) continue; + + // extract value history + List> values = extractor.extractHistory(response, options.isUseDataAggregation()); + + // check data source availability + if (values.isEmpty()) continue; + + fitValues.addAll(values); + } + return normalizer.normalize(options.getStartTime(), + options.getEndTime(), fitValues, options.getTimeInterval()); + } + + /** + * Convert input kit integer values into step content + * @param values input kit integer values + * @param startTime start time of content + * @param endTime end time of content + * @return Step content + */ + public static StepContent toStepContent(List> values, long startTime, long endTime) { + List steps = new ArrayList<>(); + if (values != null) { + for (IKValue value : values) { + steps.add(new Step( + value.getValue(), + value.getStartDate().getEpoch(), + value.getEndDate().getEpoch()) + ); + } + } + return new StepContent( + true, + startTime, + endTime, + steps + ); + } + + static class Builder { + private IFitReader fitDataReader; + private List> safeRequests; + private Options options; + private DataType dataTypeRequest; + private Pair aggregateType; + private OnCompleteListener onCompleteListener; + private OnFailureListener onFailureListener; + + Builder withFitDataReader(IFitReader fitDataReader) { + this.fitDataReader = fitDataReader; + return this; + } + + Builder addSafeRequests(List> safeRequests) { + this.safeRequests = safeRequests; + return this; + } + + Builder addOptions(Options options) { + this.options = options; + return this; + } + + Builder addDataType(DataType dataTypeRequest) { + this.dataTypeRequest = dataTypeRequest; + return this; + } + + Builder addAggregateSourceType(Pair aggregateType) { + this.aggregateType = aggregateType; + return this; + } + + Builder addOnCompleteListener(OnCompleteListener onCompleteListener) { + this.onCompleteListener = onCompleteListener; + return this; + } + + Builder addOnFailureListener(OnFailureListener onFailureListener) { + this.onFailureListener = onFailureListener; + return this; + } + + private void validate() { + if (fitDataReader == null) + throw new IllegalStateException("Fit history must be provided."); + if (safeRequests == null) + throw new IllegalStateException("Time requests must be provided."); + if (dataTypeRequest == null) + throw new IllegalStateException("Data type request must be provided."); + if (aggregateType == null) + throw new IllegalStateException("Aggregate type must be provided."); + if (options == null) + throw new IllegalStateException("Options history must be provided."); + } + + StepCountHistoryTask build() { + validate(); + return new StepCountHistoryTask( + fitDataReader, + safeRequests, + options, + dataTypeRequest, + aggregateType, + onCompleteListener, + onFailureListener + ); + } + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/sensor/DistanceSensor.java b/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/sensor/DistanceSensor.java new file mode 100644 index 0000000..11fcc49 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/sensor/DistanceSensor.java @@ -0,0 +1,35 @@ +package nl.sense.rninputkit.inputkit.googlefit.sensor; + +import android.content.Context; +import androidx.annotation.NonNull; + +import com.google.android.gms.fitness.data.DataSource; +import com.google.android.gms.fitness.data.DataType; +import com.google.android.gms.fitness.request.OnDataPointListener; + +import java.util.concurrent.TimeUnit; + +/** + * Created by panjiyudasetya on 10/23/17. + */ + +public class DistanceSensor extends SensorApi { + + public DistanceSensor(@NonNull Context context) { + super(context); + } + + void setOptions(int samplingRate, + @NonNull TimeUnit samplingTimeUnit, + @NonNull OnDataPointListener listener) { + + SensorOptions options = new SensorOptions + .Builder() + .dataType(DataType.TYPE_DISTANCE_CUMULATIVE, DataSource.TYPE_DERIVED) + .samplingRate(samplingRate) + .samplingTimeUnit(samplingTimeUnit) + .sensorListener(listener) + .build(); + setOptions(options); + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/sensor/SensorApi.java b/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/sensor/SensorApi.java new file mode 100644 index 0000000..051d110 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/sensor/SensorApi.java @@ -0,0 +1,182 @@ +package nl.sense.rninputkit.inputkit.googlefit.sensor; + +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.google.android.gms.auth.api.signin.GoogleSignIn; +import com.google.android.gms.fitness.Fitness; +import com.google.android.gms.fitness.data.DataSource; +import com.google.android.gms.fitness.request.SensorRequest; +import com.google.android.gms.tasks.OnFailureListener; +import com.google.android.gms.tasks.OnSuccessListener; +import com.google.android.gms.tasks.Task; + +import java.util.List; + +import nl.sense.rninputkit.inputkit.HealthProvider.SensorListener; +import nl.sense.rninputkit.inputkit.constant.IKStatus; +import nl.sense.rninputkit.inputkit.status.IKResultInfo; + +/** + * Created by panjiyudasetya on 6/15/17. + */ +@SuppressWarnings("weakReference") +public abstract class SensorApi { + private SensorOptions mOptions; + private Context mContext; + + public SensorApi(@NonNull Context context) { + mContext = context; + } + + /** + * Set sensor api options + * @param options {@link SensorOptions} + */ + protected void setOptions(@NonNull SensorOptions options) { + mOptions = options; + } + + /** + * Subscribing relevant Sensor + * @param listener sensor listener + * @throws IllegalStateException whenever {@link SensorApi#mOptions} unspecified. + * Make sure to call {@link SensorApi#setOptions(SensorOptions)} before subscribing. + */ + public void subscribe(@NonNull final SensorListener listener) { + if (mOptions == null) throw new IllegalStateException("Sensor options unspecified!"); + + Fitness.getSensorsClient(mContext, GoogleSignIn.getLastSignedInAccount(mContext)) + .findDataSources(mOptions.getDataSourcesRequest()) + .addOnSuccessListener(new OnSuccessListener>() { + @Override + public void onSuccess(List dataSources) { + DataSource dataSource = findDataSource(dataSources); + if (dataSource == null) { + String message = "No Data sources available for " + mOptions.getDataType().getName(); + IKResultInfo errorInfo = new IKResultInfo( + IKStatus.Code.INVALID_REQUEST, + message + ); + listener.onSubscribe(errorInfo); + return; + } + + registerSensorListener(dataSource, listener); + } + }) + .addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(@NonNull Exception e) { + IKResultInfo errorInfo = new IKResultInfo( + IKStatus.Code.INVALID_REQUEST, + e.getMessage() + ); + listener.onSubscribe(errorInfo); + } + }); + } + + /** + * Stop subscribing data from relevant Sensor synchronously + * @throws IllegalStateException whenever {@link SensorApi#mOptions} unspecified. + * Make sure to call {@link SensorApi#setOptions(SensorOptions)} before unsubscribing. + */ + public Task unsubscribe() { + if (mOptions == null) throw new IllegalStateException("Sensor options unspecified!"); + + return Fitness.getSensorsClient(mContext, GoogleSignIn.getLastSignedInAccount(mContext)) + .remove(mOptions.getSensorListener()); + } + + /** + * Stop subscribing data from relevant Sensor + * @param listener sensor listener + * @throws IllegalStateException whenever {@link SensorApi#mOptions} unspecified. + * Make sure to call {@link SensorApi#setOptions(SensorOptions)} before unsubscribing. + */ + public void unsubscribe(@NonNull final SensorListener listener) { + if (mOptions == null) throw new IllegalStateException("Sensor options unspecified!"); + + Fitness.getSensorsClient(mContext, GoogleSignIn.getLastSignedInAccount(mContext)) + .remove(mOptions.getSensorListener()) + .addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(Boolean isSuccess) { + if (isSuccess) { + IKResultInfo info = new IKResultInfo( + IKStatus.Code.VALID_REQUEST, + "Successfully remove sensor listener."); + listener.onUnsubscribe(info); + } + } + }) + .addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(@NonNull Exception e) { + IKResultInfo info = new IKResultInfo( + IKStatus.Code.INVALID_REQUEST, + e.getMessage()); + listener.onUnsubscribe(info); + } + }); + } + + /** + * Helper function to create Sensor Request on specific {@link DataSource} + * @param dataSource Sensor {@link DataSource} + * @return {@link SensorRequest} + */ + private SensorRequest buildSensorRequest(@NonNull DataSource dataSource) { + return new SensorRequest.Builder() + .setDataSource(dataSource) + .setDataType(mOptions.getDataType()) + .setSamplingRate(mOptions.getSamplingRate(), mOptions.getSamplingTimeUnit()) + .build(); + } + + /** + * Helper function to register sensor listener into Sensor API + * @param dataSource Sensor {@link DataSource} + * @param listener sensor listener + */ + private void registerSensorListener(@NonNull DataSource dataSource, + @NonNull final SensorListener listener) { + SensorRequest request = buildSensorRequest(dataSource); + Fitness.getSensorsClient(mContext, GoogleSignIn.getLastSignedInAccount(mContext)) + .add(request, mOptions.getSensorListener()) + .addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(Void aVoid) { + IKResultInfo info = new IKResultInfo( + IKStatus.Code.VALID_REQUEST, + "Successfully added sensor listener"); + listener.onSubscribe(info); + } + }) + .addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(@NonNull Exception e) { + IKResultInfo info = new IKResultInfo( + IKStatus.Code.INVALID_REQUEST, + e.getMessage()); + listener.onUnsubscribe(info); + } + }); + } + + /** + * Helper function to find a correct {@link DataSource} for relevant sensor. + * @param dataSourcesResult {@link DataSource} collection + * @return {@link DataSource} + */ + private DataSource findDataSource(@Nullable List dataSourcesResult) { + if (dataSourcesResult == null) return null; + for (DataSource dataSource : dataSourcesResult) { + if (mOptions.getDataType().equals(dataSource.getDataType())) + return dataSource; + } + return null; + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/sensor/SensorManager.java b/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/sensor/SensorManager.java new file mode 100644 index 0000000..edbd536 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/sensor/SensorManager.java @@ -0,0 +1,304 @@ +package nl.sense.rninputkit.inputkit.googlefit.sensor; + +import android.content.Context; +import androidx.annotation.NonNull; +import android.util.Pair; + +import com.google.android.gms.fitness.data.DataPoint; +import com.google.android.gms.fitness.data.Field; +import com.google.android.gms.fitness.data.Value; +import com.google.android.gms.fitness.request.OnDataPointListener; +import com.google.android.gms.tasks.Continuation; +import com.google.android.gms.tasks.OnFailureListener; +import com.google.android.gms.tasks.OnSuccessListener; +import com.google.android.gms.tasks.Task; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import nl.sense.rninputkit.inputkit.HealthProvider.SensorListener; +import nl.sense.rninputkit.inputkit.constant.IKStatus; +import nl.sense.rninputkit.inputkit.constant.SampleType; +import nl.sense.rninputkit.inputkit.constant.SampleType.SampleName; +import nl.sense.rninputkit.inputkit.entity.DateContent; +import nl.sense.rninputkit.inputkit.entity.IKValue; +import nl.sense.rninputkit.inputkit.entity.SensorDataPoint; +import nl.sense.rninputkit.inputkit.status.IKResultInfo; + +import static nl.sense.rninputkit.inputkit.constant.DataSampling.DEFAULT_SAMPLING_TIME_UNIT; +import static nl.sense.rninputkit.inputkit.constant.DataSampling.DEFAULT_TIME_SAMPLING_RATE; + +/** + * Created by panjiyudasetya on 7/24/17. + */ + +public class SensorManager { + private StepSensor mStepSensor; + private DistanceSensor mDistanceSensor; + private Context mContext; + private Map> mSensorListeners; + // Step tracking data point listener + private final OnDataPointListener mStepDataPointListener = new OnDataPointListener() { + @Override + public void onDataPoint(DataPoint dataPoint) { + SensorListener listener = mSensorListeners.get(SampleType.STEP_COUNT); + if (dataPoint != null && listener != null) { + listener.onReceive( + fromDataPoint( + SampleType.STEP_COUNT, + dataPoint + ) + ); + } + } + }; + // Distance walking or running data point listener + private final OnDataPointListener mDistanceDataPointListener = new OnDataPointListener() { + @Override + public void onDataPoint(DataPoint dataPoint) { + SensorListener listener = mSensorListeners.get(SampleType.DISTANCE_WALKING_RUNNING); + if (dataPoint != null && listener != null) { + listener.onReceive( + fromDataPoint( + SampleType.DISTANCE_WALKING_RUNNING, + dataPoint + ) + ); + } + } + }; + + public SensorManager(@NonNull Context context) { + mContext = context; + mStepSensor = new StepSensor(mContext); + mDistanceSensor = new DistanceSensor(mContext); + mSensorListeners = new HashMap<>(); + } + + /** + * Register sensor API listener + * @param sampleType sensor type name + * @param listener sensor data point listener + */ + @SuppressWarnings("unused") + public void registerListener(@NonNull @SampleName String sampleType, + @NonNull SensorListener listener) { + mSensorListeners.put(sampleType, listener); + } + + /** + * Start sensor tracking Api based on specific sensor. + * @param sampleType available sensor + * @param samplingRate sensor sampling rate. + * Sensor will be started every X-Time Unit, for instance : { 5, {@link TimeUnit#MINUTES} }. + * If sampling rate is unspecified it will be set to 10 minute interval. + */ + @SuppressWarnings("unused") + public void startTracking(@NonNull @SampleName String sampleType, + @NonNull Pair samplingRate) { + if (sampleType.equals(SampleType.STEP_COUNT)) { + startStepTracking(samplingRate); + } else if (sampleType.equals(SampleType.DISTANCE_WALKING_RUNNING)) { + startDistanceTracking(samplingRate); + } + } + + /** + * Stop sensor tracking Api based on specific sensor. + * @param sampleType available sensor + */ + @SuppressWarnings("unused") + public void stopTracking(@NonNull @SampleName String sampleType) { + if (sampleType.equals(SampleType.STEP_COUNT)) { + stopStepTracking(); + } else if (sampleType.equals(SampleType.DISTANCE_WALKING_RUNNING)) { + stopDistanceTracking(); + } + } + + /** + * Stop all sensor tracking. + */ + @SuppressWarnings("unused") + public void stopTrackingAll(@NonNull final SensorListener listener) { + mStepSensor.unsubscribe() + .continueWithTask(new Continuation>() { + @Override + public Task then(@NonNull Task task) { + return mDistanceSensor.unsubscribe(); + } + }) + .addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(Boolean result) { + handleResponse(true, + listener, + SampleType.STEP_COUNT, + SampleType.DISTANCE_WALKING_RUNNING); + } + }) + .addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(@NonNull Exception e) { + handleResponse(false, + listener, + SampleType.STEP_COUNT, + SampleType.DISTANCE_WALKING_RUNNING); + } + }); + } + + private void handleResponse(boolean isSuccess, + @NonNull SensorListener listener, + @NonNull String... sensorTypes) { + String message; + int status; + if (isSuccess) { + status = IKStatus.Code.VALID_REQUEST; + message = String.format("%s sensor samples has been stopped.", + Arrays.toString(sensorTypes)); + } else { + status = IKStatus.Code.INVALID_REQUEST; + message = String.format("%s sensor samples has been stopped.", + Arrays.toString(sensorTypes)); + } + listener.onUnsubscribe(new IKResultInfo(status, message)); + } + + /** + * Helper function to start step count sensor api + * @param samplingRate sensor sampling rate. + * Sensor will be started every X-Time Unit, for instance : { 5, {@link TimeUnit#MINUTES} }. + * If sampling rate is unspecified it will be set to 10 minute interval. + */ + private void startStepTracking(@NonNull Pair samplingRate) { + final SensorListener listener = mSensorListeners.get(SampleType.STEP_COUNT); + if (listener == null) { + String message = getStartFailureMessage(SampleType.STEP_COUNT); + throw new IllegalStateException(message); + } + + int rate = (samplingRate.first == null || samplingRate.first <= 0) + ? DEFAULT_TIME_SAMPLING_RATE : samplingRate.first; + TimeUnit timeUnit = samplingRate.second == null + ? DEFAULT_SAMPLING_TIME_UNIT : samplingRate.second; + mStepSensor.setOptions(rate, timeUnit, mStepDataPointListener); + mStepSensor.subscribe(listener); + } + + /** + * Helper function to stop step count sensor api + */ + private void stopStepTracking() { + final SensorListener listener = mSensorListeners.get(SampleType.STEP_COUNT); + if (listener == null) { + String message = getStartFailureMessage(SampleType.STEP_COUNT); + throw new IllegalStateException(message); + } + + mStepSensor.unsubscribe(listener); + } + + /** + * Helper function to start distance walking or running sensor api + * @param samplingRate sensor sampling rate. + * Sensor will be started every X-Time Unit, for instance : { 5, {@link TimeUnit#MINUTES} }. + * If sampling rate is unspecified it will be set to 10 minute interval. + */ + private void startDistanceTracking(@NonNull Pair samplingRate) { + final SensorListener listener = mSensorListeners.get(SampleType.DISTANCE_WALKING_RUNNING); + if (listener == null) { + String message = getStartFailureMessage(SampleType.DISTANCE_WALKING_RUNNING); + throw new IllegalStateException(message); + } + + int rate = (samplingRate.first == null || samplingRate.first <= 0) + ? DEFAULT_TIME_SAMPLING_RATE : samplingRate.first; + TimeUnit timeUnit = samplingRate.second == null + ? DEFAULT_SAMPLING_TIME_UNIT : samplingRate.second; + mDistanceSensor.setOptions(rate, timeUnit, mDistanceDataPointListener); + + mDistanceSensor.subscribe(listener); + } + + /** + * Helper function to stop distance walking or running sensor api + */ + private void stopDistanceTracking() { + final SensorListener listener = mSensorListeners.get(SampleType.DISTANCE_WALKING_RUNNING); + if (listener == null) { + String message = getStartFailureMessage(SampleType.DISTANCE_WALKING_RUNNING); + throw new IllegalStateException(message); + } + + mDistanceSensor.unsubscribe(listener); + } + + /** + * Helper function to generate failure message + * @param sensorType sensor type name + * @return failure message + */ + private String getStartFailureMessage(@NonNull @SampleName String sensorType) { + return "UNABLE TO PERFORM THIS ACTION!\n" + + "Please do register sensor listener for " + sensorType + + " before starting to monitor this event."; + } + + /** + * Return payload collections from given data point. + * @param sensorType sensor type name + * @param dataPoint Event data point + * @return {@link SensorDataPoint} + */ + private static SensorDataPoint fromDataPoint(@NonNull @SampleName String sensorType, + @NonNull DataPoint dataPoint) { + + List> payloads = new ArrayList<>(); + SensorDataPoint output = new SensorDataPoint( + sensorType, + Collections.>emptyList() + ); + + if (dataPoint.getDataType() == null) return output; + + List fields = dataPoint.getDataType().getFields(); + if (fields == null || fields.isEmpty()) return output; + + for (Field field : fields) { + Value value = dataPoint.getValue(field); + int format = value.getFormat(); + switch (format) { + case Field.FORMAT_FLOAT: + payloads.add(new IKValue<>( + value.asFloat(), + new DateContent(dataPoint.getStartTime(TimeUnit.MILLISECONDS)), + new DateContent(dataPoint.getEndTime(TimeUnit.MILLISECONDS))) + ); + break; + case Field.FORMAT_INT32: + payloads.add(new IKValue<>( + value.asInt(), + new DateContent(dataPoint.getStartTime(TimeUnit.MILLISECONDS)), + new DateContent(dataPoint.getEndTime(TimeUnit.MILLISECONDS))) + ); + break; + case Field.FORMAT_STRING: + default: + payloads.add(new IKValue<>( + value.asString(), + new DateContent(dataPoint.getStartTime(TimeUnit.MILLISECONDS)), + new DateContent(dataPoint.getEndTime(TimeUnit.MILLISECONDS))) + ); + break; + } + } + output.setPayload(payloads); + return output; + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/sensor/SensorOptions.java b/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/sensor/SensorOptions.java new file mode 100644 index 0000000..2a200de --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/sensor/SensorOptions.java @@ -0,0 +1,105 @@ +package nl.sense.rninputkit.inputkit.googlefit.sensor; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.google.android.gms.fitness.data.DataSource; +import com.google.android.gms.fitness.data.DataType; +import com.google.android.gms.fitness.request.DataSourcesRequest; +import com.google.android.gms.fitness.request.OnDataPointListener; + +import java.util.concurrent.TimeUnit; + +import static nl.sense.rninputkit.inputkit.constant.DataSampling.DEFAULT_SAMPLING_TIME_UNIT; +import static nl.sense.rninputkit.inputkit.constant.DataSampling.DEFAULT_TIME_SAMPLING_RATE; +import static nl.sense.rninputkit.inputkit.googlefit.sensor.Validator.validateDataType; +import static nl.sense.rninputkit.inputkit.googlefit.sensor.Validator.validateSensorListener; + +/** + * Created by panjiyudasetya on 6/15/17. + */ + +public class SensorOptions { + private DataType mDataType; + private DataSourcesRequest mDataSourcesRequest; + private int mSamplingRate; + private TimeUnit mSamplingTimeUnit; + private OnDataPointListener mSensorListener; + + private SensorOptions(@NonNull DataType dataType, + @NonNull DataSourcesRequest dataSourcesRequest, + int timeSampling, + @NonNull TimeUnit samplingTimeUnit, + @NonNull OnDataPointListener sensorListener) { + mDataType = dataType; + mDataSourcesRequest = dataSourcesRequest; + mSamplingRate = timeSampling; + mSamplingTimeUnit = samplingTimeUnit; + mSensorListener = sensorListener; + } + + public DataType getDataType() { + return mDataType; + } + + public DataSourcesRequest getDataSourcesRequest() { + return mDataSourcesRequest; + } + + public int getSamplingRate() { + return mSamplingRate; + } + + public TimeUnit getSamplingTimeUnit() { + return mSamplingTimeUnit; + } + + public OnDataPointListener getSensorListener() { + return mSensorListener; + } + + public static class Builder { + private DataType newDataType; + private DataSourcesRequest newDataSourcesRequest; + private int newSamplingRate; + private TimeUnit newSamplingTimeUnit; + private OnDataPointListener newSensorListener; + + public Builder dataType(@NonNull DataType dataType, int dataSourceType) { + newDataType = dataType; + newDataSourcesRequest = new DataSourcesRequest.Builder() + .setDataTypes(dataType) + .setDataSourceTypes(dataSourceType < 0 ? DataSource.TYPE_RAW : dataSourceType) + .build(); + return this; + } + + public Builder samplingRate(int samplingRate) { + newSamplingRate = samplingRate; + return this; + } + + public Builder samplingTimeUnit(@Nullable TimeUnit samplingTimeUnit) { + newSamplingTimeUnit = samplingTimeUnit; + return this; + } + + public Builder sensorListener(@NonNull OnDataPointListener sensorListener) { + newSensorListener = sensorListener; + return this; + } + + public SensorOptions build() { + validateDataType(newDataType); + validateSensorListener(newSensorListener); + + return new SensorOptions( + newDataType, + newDataSourcesRequest, + newSamplingRate == 0 ? DEFAULT_TIME_SAMPLING_RATE : newSamplingRate, + newSamplingTimeUnit == null ? DEFAULT_SAMPLING_TIME_UNIT : newSamplingTimeUnit, + newSensorListener + ); + } + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/sensor/StepSensor.java b/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/sensor/StepSensor.java new file mode 100644 index 0000000..2600873 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/sensor/StepSensor.java @@ -0,0 +1,35 @@ +package nl.sense.rninputkit.inputkit.googlefit.sensor; + +import android.content.Context; +import androidx.annotation.NonNull; + +import com.google.android.gms.fitness.data.DataSource; +import com.google.android.gms.fitness.data.DataType; +import com.google.android.gms.fitness.request.OnDataPointListener; + +import java.util.concurrent.TimeUnit; + +/** + * Created by panjiyudasetya on 7/20/17. + */ + +public class StepSensor extends SensorApi { + + public StepSensor(@NonNull Context context) { + super(context); + } + + void setOptions(int samplingRate, + @NonNull TimeUnit samplingTimeUnit, + @NonNull OnDataPointListener listener) { + + SensorOptions options = new SensorOptions + .Builder() + .dataType(DataType.TYPE_STEP_COUNT_DELTA, DataSource.TYPE_DERIVED) + .samplingRate(samplingRate) + .samplingTimeUnit(samplingTimeUnit) + .sensorListener(listener) + .build(); + setOptions(options); + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/sensor/Validator.java b/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/sensor/Validator.java new file mode 100644 index 0000000..15578d9 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/googlefit/sensor/Validator.java @@ -0,0 +1,25 @@ +package nl.sense.rninputkit.inputkit.googlefit.sensor; + +import com.google.android.gms.fitness.data.DataType; +import com.google.android.gms.fitness.request.OnDataPointListener; + +/** + * Created by panjiyudasetya on 6/15/17. + */ + +@SuppressWarnings("SpellCheckingInspection") +public class Validator { + private Validator() { } + + public static void validateDataType(DataType dataType) { + if (dataType == null) { + throw new IllegalStateException("Sensor data type must be provided!"); + } + } + + public static void validateSensorListener(OnDataPointListener listener) { + if (listener == null) { + throw new IllegalStateException("Sensor listener must be provided!"); + } + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/helper/AppHelper.java b/android/src/main/java/nl/sense/rninputkit/inputkit/helper/AppHelper.java new file mode 100644 index 0000000..900110e --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/helper/AppHelper.java @@ -0,0 +1,77 @@ +package nl.sense.rninputkit.inputkit.helper; + +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import androidx.annotation.NonNull; + +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.GoogleApiAvailability; + +import static nl.sense.rninputkit.inputkit.constant.RequiredApp.GOOGLE_FIT_PACKAGE_NAME; + +/** + * Created by panjiyudasetya on 9/26/17. + */ + +public class AppHelper { + private AppHelper() { } + /** + * Check whether Google Fit application is installed on the device or not. + * @param context Current application context + * @return True if installed False otherwise + */ + public static boolean isGoogleFitInstalled(@NonNull Context context) { + try { + context.getPackageManager().getPackageInfo(GOOGLE_FIT_PACKAGE_NAME, PackageManager.GET_ACTIVITIES); + return true; + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + + /** + * Check whether Google Play service is up to date or not + * @param context Current application context + * @return True if up-to-date, False otherwise + */ + public static boolean isPlayServiceUpToDate(@NonNull Context context) { + GoogleApiAvailability googleApiAvailability = GoogleApiAvailability.getInstance(); + Integer resultCode = googleApiAvailability.isGooglePlayServicesAvailable(context); + return resultCode != ConnectionResult.SUCCESS ? false : true; + } + + /** + * Open required application package in playstore if available + * @param context Current application context + * @param packageId Application package id + */ + public static void openInPlayStore(@NonNull Context context, @NonNull String packageId) { + final String LINK_TO_GOOGLE_PLAY_SERVICES = "play.google.com/store/apps/details?id=" + packageId + "&hl=en"; + try { + context.startActivity(new Intent( + Intent.ACTION_VIEW, + Uri.parse("market://" + LINK_TO_GOOGLE_PLAY_SERVICES) + )); + } catch (ActivityNotFoundException e) { + context.startActivity(new Intent( + Intent.ACTION_VIEW, + Uri.parse("https://" + LINK_TO_GOOGLE_PLAY_SERVICES) + )); + } + } + + /** + * Launch another application in phone + * @param context Current application context + * @param packageId Application package id + */ + public static void openAnotherApp(@NonNull Context context, @NonNull String packageId) { + Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(packageId); + if (launchIntent != null) { + context.startActivity(launchIntent); + } + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/helper/CollectionUtils.java b/android/src/main/java/nl/sense/rninputkit/inputkit/helper/CollectionUtils.java new file mode 100644 index 0000000..f78d825 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/helper/CollectionUtils.java @@ -0,0 +1,55 @@ +package nl.sense.rninputkit.inputkit.helper; + +import androidx.annotation.NonNull; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import nl.sense.rninputkit.inputkit.entity.IKValue; + +/** + * Created by panjiyudasetya on 7/3/17. + */ + +public class CollectionUtils { + /** + * Helper function to sort steps collections. + * + * @param ascending Set to True to use ascending sort, + * False to use descending + * @param values Input kit values + */ + public static void sort(boolean ascending, @NonNull List> values) { + // create comparator based on sorted type + Comparator comparator; + if (ascending) { + comparator = new Comparator() { + @Override + public int compare(IKValue ikValue1, IKValue ikValue2) { + return compareLong(ikValue1.getStartDate().getEpoch(), ikValue2.getStartDate().getEpoch()); + } + }; + } else { + comparator = new Comparator() { + @Override + public int compare(IKValue ikValue1, IKValue ikValue2) { + return compareLong(ikValue2.getStartDate().getEpoch(), ikValue1.getStartDate().getEpoch()); + } + }; + } + + Collections.sort(values, comparator); + } + + /** + * Helper function to compare two long values + * @param value1 First value to compare + * @param value2 Second value to compare + * @return int result + */ + @SuppressWarnings("PMD") // Long.compare(value1, value2) is no available on API 16 + private static int compareLong(Long value1, Long value2) { + return value1.compareTo(value2); + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/helper/InputKitTimeUtils.java b/android/src/main/java/nl/sense/rninputkit/inputkit/helper/InputKitTimeUtils.java new file mode 100644 index 0000000..6600958 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/helper/InputKitTimeUtils.java @@ -0,0 +1,238 @@ +package nl.sense.rninputkit.inputkit.helper; + +import androidx.annotation.NonNull; +import android.util.Pair; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.TimeUnit; + +import nl.sense.rninputkit.inputkit.InputKit.Result; +import nl.sense.rninputkit.inputkit.constant.IKStatus; +import nl.sense.rninputkit.inputkit.entity.TimeInterval; +import nl.sense.rninputkit.inputkit.status.IKResultInfo; + +/** + * Created by panjiyudasetya on 6/19/17. + */ + +public class InputKitTimeUtils { + public static final long ONE_DAY = 24 * 60 * 60 * 1000; + + private InputKitTimeUtils() { + } + + /** + * Get minute difference between two timestamp values. + * @param timeStamp1 First timestamp + * @param timeStamp2 Second timestamp + * @return A minute difference + */ + public static long getMinuteDiff(long timeStamp1, long timeStamp2) { + // Make sure to exclude milliseconds calculation + timeStamp1 = timeStamp1 / 1000 * 1000; + timeStamp2 = timeStamp2 / 1000 * 1000; + return Math.abs(TimeUnit.MILLISECONDS.toMinutes(timeStamp2 - timeStamp1)); + } + + /** + * Helper function to detect whether given start and end time are overlapping time window. + * @param startTime Start time + * @param endTime End time + * @param time Time window + * @return True if overlapping time window, False otherwise. + */ + public static boolean isOverlappingTimeWindow(long startTime, + long endTime, + @NonNull Pair time) { + return (startTime < time.first && endTime >= time.first) + || (startTime < time.second && endTime >= time.second); + } + + /** + * Helper function to detect whether given start and end time are within time window or not. + * + * @param startTime Start time + * @param endTime End time + * @param time Time window + * @return True if within time window, False otherwise. + */ + public static boolean isWithinTimeWindow(long startTime, + long endTime, + @NonNull Pair time) { + return isWithinTimeWindow(startTime, time) && isWithinTimeWindow(endTime, time); + } + + /** + * Check is a given time within time period or not. + * @param time1 Timestamp that needs to be checked + * @param timePeriod Bound of time period + * @return True if a given time within time period, False otherwise. + */ + public static boolean isWithinTimeWindow(long time1, @NonNull Pair timePeriod) { + return time1 >= timePeriod.first && time1 < timePeriod.second; + } + + /** + * Helper function to populate time window based on specific range and {@link TimeInterval}. + * For instance, to get time window for each ten minute starting from specific time, it can be + * achieved by call : + *

{@code
+     *
+     *     Pair timeRange = InputKitTimeUtils.populateOneDayRangeBeforeGivenTime(
+     *          new Date().getTimeInMillis()
+     *     );
+     *
+     *     List> timeWindow = InputKitTimeUtils.populateTimeWindows(
+     *          timeRange.first,
+     *          timeRange.second,
+     *          new TimeInterval({@link nl.sense.rninputkit.inputkit.constant.Interval#TEN_MINUTE})
+     *     );
+     * }
+ * Then the output should be like (in human readable format) : + *
{@code
+     *
+     *     [{"2017-06-13 12:40:00", "2017-06-13 12:50:00"}, {"2017-06-13 12:30:00", "2017-06-13 12:40:00"}]
+     * }
+ * + * @param startTime Start time + * @param endTime End time + * @param interval {@link TimeInterval} + * @return Time window + */ + public static List> populateTimeWindows(long startTime, + long endTime, + @NonNull TimeInterval interval) { + validateTimeInput(startTime, endTime); + + List> timeWindows = new ArrayList<>(); + while (startTime < endTime) { + long relativeEndTime = computeTimeWindow(startTime, interval); + if (relativeEndTime > endTime) relativeEndTime = endTime; + timeWindows.add(Pair.create(startTime, relativeEndTime)); + startTime = relativeEndTime; + } + return timeWindows; + } + + /** + * Validate given time period + * + * @param startTime Start time + * @param endTime End time + * @param callback {@link Result} callback which to be handled + * @return True if valid time period, False otherwise. + */ + public static boolean validateTimeInput(long startTime, + long endTime, + @NonNull Result callback) { + + if (!isValidTimePeriod(startTime, endTime)) { + callback.onError(new IKResultInfo( + IKStatus.Code.INVALID_REQUEST, + "Invalid time period. Start time and end time should be greater than 0!" + )); + return false; + } + + if (!isValidStartTime(startTime, endTime)) { + callback.onError(new IKResultInfo( + IKStatus.Code.INVALID_REQUEST, + "Invalid time period. Start time should less or equals than end time!" + )); + return false; + } + return true; + } + + /** + * Helper function to compute time based on {@link TimeInterval} + * + * @param anchorTime Anchor time + * @param interval {@link TimeInterval} + * @return Previous time of known end time + */ + public static long computeTimeWindow(long anchorTime, @NonNull TimeInterval interval) { + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(anchorTime); + + // set calendar operator based on given time interval + if (interval.getTimeUnit().equals(TimeUnit.DAYS)) { + cal.add(Calendar.DAY_OF_MONTH, interval.getValue()); + } else if (interval.getTimeUnit().equals(TimeUnit.HOURS)) { + cal.add(Calendar.HOUR_OF_DAY, interval.getValue()); + } else if (interval.getTimeUnit().equals(TimeUnit.MINUTES)) { + cal.add(Calendar.MINUTE, interval.getValue()); + } else + throw new IllegalStateException("Unsupported Time Interval detected!\n" + interval.toString()); + + return cal.getTimeInMillis(); + } + + /** + * get epoch time of today in current time zone + */ + public static long getTodayStartTime() { + Calendar today = Calendar.getInstance(); + today.set(Calendar.HOUR_OF_DAY, 0); + today.set(Calendar.MINUTE, 0); + today.set(Calendar.SECOND, 0); + today.set(Calendar.MILLISECOND, 0); + return today.getTimeInMillis(); + } + + /** + * Helper function to validate input time. + * + * @param startTime Given start time + * @param endTime Given end time + * @throws {@link IllegalStateException} + */ + private static void validateTimeInput(long startTime, long endTime) { + if (!isValidTimePeriod(startTime, endTime)) { + throw new IllegalStateException("Start time and end time should be greater than 0!"); + } + if (!isValidStartTime(startTime, endTime)) { + throw new IllegalStateException("Start time should less or equals than end time!"); + } + } + + /** + * Validate time period. + * + * @param startTime Given start time + * @param endTime Given end time + * @return True if valid, false otherwise. + */ + private static boolean isValidTimePeriod(long startTime, long endTime) { + return (startTime > 0 && endTime > 0); + } + + /** + * Validate start time value. + * + * @param startTime Given start time + * @param endTime Given end time + * @return True if valid, false otherwise. + */ + private static boolean isValidStartTime(long startTime, long endTime) { + return startTime <= endTime; + } + + /** + * Helper function to convert time in human readable format + * + * @param stamp Given start time + * example output : 02-Oct-2017 12:30:00 + */ + public static String timeStampToString(long stamp) { + final String TIME_FORMAT = "dd-MMM-yyyy HH:mm:ss"; + Calendar c = Calendar.getInstance(); + c.setTimeInMillis(stamp); + SimpleDateFormat dateFormat = new SimpleDateFormat(TIME_FORMAT, Locale.US); + return dateFormat.format(c.getTime()); + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/helper/PreferenceHelper.java b/android/src/main/java/nl/sense/rninputkit/inputkit/helper/PreferenceHelper.java new file mode 100644 index 0000000..e38a23f --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/helper/PreferenceHelper.java @@ -0,0 +1,75 @@ +package nl.sense.rninputkit.inputkit.helper; + +import android.content.Context; +import android.content.SharedPreferences; +import androidx.annotation.NonNull; +import android.text.TextUtils; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +/** + * Created by panjiyudasetya on 7/21/17. + */ + +public class PreferenceHelper { + private PreferenceHelper() { } + + private static final String PREFERENCE_NAME = "IK_PREFERENCE"; + + /** + * Add value string into Shared Preference. + * For non string value, just convert it into {@link String} + * For an object, use {@link Gson} to stringify the object. + * + * @param context current application context + * @param key Key preference + * @param value Value preference + */ + public static void add(@NonNull Context context, + @NonNull String key, + String value) { + SharedPreferences.Editor editor = context.getSharedPreferences( + PREFERENCE_NAME, + Context.MODE_PRIVATE + ).edit(); + editor.putString(key, value); + editor.apply(); + } + + /** + * Get value from Shared Preference. + * + * @param context current application context + * @param key Key preference + * @return value string. + */ + public static String get(@NonNull Context context, + @NonNull String key) { + SharedPreferences preferences = context.getSharedPreferences( + PREFERENCE_NAME, + Context.MODE_PRIVATE + ); + return preferences.getString(key, null); + } + + /** + * Get value from Shared Preference. + * + * @param context current application context + * @param key Key preference + * @return {@link JsonObject} jsonify value from share preference. + */ + public static JsonObject getAsJson(@NonNull Context context, + @NonNull String key) { + SharedPreferences preferences = context.getSharedPreferences( + PREFERENCE_NAME, + Context.MODE_PRIVATE + ); + String json = preferences.getString(key, null); + return TextUtils.isEmpty(json) + ? new JsonObject() + : new JsonParser().parse(json).getAsJsonObject(); + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/shealth/BloodPressureReader.java b/android/src/main/java/nl/sense/rninputkit/inputkit/shealth/BloodPressureReader.java new file mode 100644 index 0000000..637c14f --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/shealth/BloodPressureReader.java @@ -0,0 +1,80 @@ +package nl.sense.rninputkit.inputkit.shealth; + +import androidx.annotation.NonNull; + +import com.samsung.android.sdk.healthdata.HealthConstants; +import com.samsung.android.sdk.healthdata.HealthData; +import com.samsung.android.sdk.healthdata.HealthDataResolver; +import com.samsung.android.sdk.healthdata.HealthDataStore; +import com.samsung.android.sdk.healthdata.HealthResultHolder; + +import java.util.ArrayList; +import java.util.List; + +import nl.sense.rninputkit.inputkit.InputKit; +import nl.sense.rninputkit.inputkit.constant.IKStatus; +import nl.sense.rninputkit.inputkit.entity.BloodPressure; +import nl.sense.rninputkit.inputkit.status.IKResultInfo; + +/** + * Created by xedi on 10/9/17. + */ + +public class BloodPressureReader { + private final HealthDataResolver mResolver; + + public BloodPressureReader(HealthDataStore store) { + mResolver = new HealthDataResolver(store, null); + } + + public void readBloodPressure(final long startTime, final long stopTime, + @NonNull final InputKit.Result> callback) { + try { + HealthDataResolver.ReadRequest request = createRequest(startTime, stopTime); + mResolver.read(request).setResultListener( + new HealthResultHolder.ResultListener() { + @Override + public void onResult(HealthDataResolver.ReadResult healthDatas) { + List bloodPressureList = new ArrayList(); + try { + for (HealthData data : healthDatas) { + Long time = data.getLong(HealthConstants.BloodPressure.START_TIME); + Integer sys = data.getInt(HealthConstants.BloodPressure.SYSTOLIC); + Integer dia = data.getInt(HealthConstants.BloodPressure.DIASTOLIC); + Float mean = data.getFloat(HealthConstants.BloodPressure.MEAN); + Integer pulse = data.getInt(HealthConstants.BloodPressure.PULSE); + String comment = data.getString(HealthConstants.BloodPressure.COMMENT); + + BloodPressure bp = new BloodPressure(sys, dia, time); + bp.setMean(mean); + bp.setPulse(pulse); + bp.setComment(comment); + bloodPressureList.add(bp); + } + } finally { + callback.onNewData(bloodPressureList); + healthDatas.close(); + } + } + }); + } catch (Exception e) { + callback.onError(new IKResultInfo(IKStatus.Code.UNKNOWN_ERROR, e.getMessage())); + } + } + + private HealthDataResolver.ReadRequest createRequest(long startTime, long stopTime) { + return new HealthDataResolver.ReadRequest.Builder() + .setDataType(HealthConstants.BloodPressure.HEALTH_DATA_TYPE) + .setProperties(new String[]{HealthConstants.BloodPressure.DEVICE_UUID, + HealthConstants.BloodPressure.START_TIME, + HealthConstants.BloodPressure.SYSTOLIC, + HealthConstants.BloodPressure.DIASTOLIC, + HealthConstants.BloodPressure.MEAN, + HealthConstants.BloodPressure.PULSE, + HealthConstants.BloodPressure.COMMENT}) + .setLocalTimeRange(HealthConstants.BloodPressure.START_TIME, HealthConstants.BloodPressure.TIME_OFFSET, + startTime, stopTime) + .setSort(HealthConstants.BloodPressure.START_TIME, HealthDataResolver.SortOrder.DESC) + .build(); + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/shealth/SHealthConstant.java b/android/src/main/java/nl/sense/rninputkit/inputkit/shealth/SHealthConstant.java new file mode 100644 index 0000000..396316d --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/shealth/SHealthConstant.java @@ -0,0 +1,42 @@ +package nl.sense.rninputkit.inputkit.shealth; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Created by xedi on 9/25/17. + */ + +public class SHealthConstant { + public static final String STEP_COUNT = "com.samsung.health.step_count"; + public static final String STEP_DAILY_TREND = "com.samsung.shealth.step_daily_trend"; + + public static final int STATUS_CONNECTED = 0; + public static final int STATUS_DISCONNECTED = 1; + public static final int STATUS_ERROR = 2; + public static final int STATUS_ERROR_INIT = 3; + + public static final int PERMISSION_GRANTED = 0; + public static final int PERMISSION_DENIED = 1; + + public static final long ONE_MINUTE = 60 * 1000; + public static final long ONE_HOUR = ONE_MINUTE * 60; + public static final long ONE_DAY = ONE_HOUR * 24; + + public static final String ASLEEP = "asleep"; + public static final String AWAKE = "awake"; + public static final String IN_BED = "inBed"; + + public static final List SUPPORTED_DATA_TYPES = + new ArrayList<>(Arrays.asList("step_count", + "step_history", + "sleep", + "weight", + "blood_pressure")); + + public static final String DATE_FORMAT_DAILY = "yyyy-MM-dd"; + public static final String DATE_FORMAT_HOURLY = "yyyy-MM-dd HH"; + public static final String DATE_FORMAT_MINUTELY = "yyyy-MM-dd HH:mm"; + +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/shealth/SHealthWrapper.java b/android/src/main/java/nl/sense/rninputkit/inputkit/shealth/SHealthWrapper.java new file mode 100644 index 0000000..0ba3e94 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/shealth/SHealthWrapper.java @@ -0,0 +1,308 @@ +package nl.sense.rninputkit.inputkit.shealth; + +import android.content.Context; +import androidx.annotation.NonNull; +import android.util.Log; + +import com.samsung.android.sdk.healthdata.HealthConnectionErrorResult; +import com.samsung.android.sdk.healthdata.HealthDataService; +import com.samsung.android.sdk.healthdata.HealthDataStore; +import com.samsung.android.sdk.healthdata.HealthPermissionManager; +import com.samsung.android.sdk.healthdata.HealthResultHolder; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import nl.sense.rninputkit.inputkit.HealthProvider; +import nl.sense.rninputkit.inputkit.InputKit; +import nl.sense.rninputkit.inputkit.constant.IKStatus; +import nl.sense.rninputkit.inputkit.constant.SampleType; +import nl.sense.rninputkit.inputkit.entity.BloodPressure; +import nl.sense.rninputkit.inputkit.entity.IKValue; +import nl.sense.rninputkit.inputkit.entity.SensorDataPoint; +import nl.sense.rninputkit.inputkit.entity.StepContent; +import nl.sense.rninputkit.inputkit.entity.Weight; +import nl.sense.rninputkit.inputkit.Options; +import nl.sense.rninputkit.inputkit.shealth.utils.SHealthPermissionSet; +import nl.sense.rninputkit.inputkit.status.IKResultInfo; + + +/** + * Created by xedi on 9/20/17. + */ + +public class SHealthWrapper { + public static final String TAG = "SHealthWrapper"; + private HealthDataStore mStore; + private StepCountReader mStepReader; + private SleepReader mSleepReader; + private BloodPressureReader mBloodPressureReader; + private WeightReader mWeightReader; + private SHealthPermissionSet mPermissionSet; + + private boolean mFinished = true; + private int mConnectionStatus = SHealthConstant.STATUS_DISCONNECTED; + private HealthConnectionErrorResult mLastConnectionError = null; + private OnConnectCallback mCurrentConnectCallback = null; + private final HealthDataStore.ConnectionListener mConnectionListener = + new HealthDataStore.ConnectionListener() { + @Override + public void onConnected() { + mConnectionStatus = SHealthConstant.STATUS_CONNECTED; + mLastConnectionError = null; + if (mCurrentConnectCallback != null) { + mCurrentConnectCallback.onResult(mConnectionStatus, null); + } + Log.d(TAG, "onConnected"); + } + + @Override + public void onConnectionFailed(HealthConnectionErrorResult error) { + mConnectionStatus = SHealthConstant.STATUS_ERROR; + mLastConnectionError = error; + if (mCurrentConnectCallback != null) { + mCurrentConnectCallback.onResult(mConnectionStatus, mLastConnectionError); + } + Log.d(TAG, "onConnectionFailed"); + } + + @Override + public void onDisconnected() { + mConnectionStatus = SHealthConstant.STATUS_DISCONNECTED; + mLastConnectionError = null; + if (mCurrentConnectCallback != null) { + mCurrentConnectCallback.onResult(mConnectionStatus, null); + } + Log.d(TAG, "onDisconnected"); + if (!isFinishing()) { + mStore.connectService(); + } + } + }; + private OnPermissionCallback mCurrentPermissionCallback = null; + private final HealthResultHolder.ResultListener mPermissionListener = + new HealthResultHolder.ResultListener() { + @Override + public void onResult(HealthPermissionManager.PermissionResult result) { + Map resultMap = result.getResultMap(); + if (resultMap.values().contains(Boolean.FALSE)) { + mCurrentPermissionCallback.onResult(SHealthConstant.PERMISSION_DENIED); + } else { + mCurrentPermissionCallback.onResult(SHealthConstant.PERMISSION_GRANTED); + } + } + }; + + public SHealthWrapper(Context context) { + mFinished = false; + mPermissionSet = SHealthPermissionSet.getInstance(); + initialize(context); + initReporter(); + } + + private void initialize(Context context) { + HealthDataService healthDataService = new HealthDataService(); + try { + healthDataService.initialize(context); + } catch (Exception e) { + e.printStackTrace(); + mConnectionStatus = SHealthConstant.STATUS_ERROR_INIT; + } + mStore = new HealthDataStore(context, mConnectionListener); + mStore.connectService(); + } + + public void connectService(OnConnectCallback connectCallback) { + mCurrentConnectCallback = connectCallback; + if (mConnectionStatus == SHealthConstant.STATUS_CONNECTED) { + connectCallback.onResult(mConnectionStatus, mLastConnectionError); + } else { + mStore.connectService(); + } + } + + public void disconnect() { + mConnectionStatus = SHealthConstant.STATUS_DISCONNECTED; + mLastConnectionError = null; + mStore.disconnectService(); + } + + public boolean isFinishing() { + return mFinished; + } + + public HealthConnectionErrorResult getLastConnectionError() { + return mLastConnectionError; + } + + public int getConnectionStatus() { + return mConnectionStatus; + } + + public void authorize(OnPermissionCallback permissionCallback, + boolean forceShowPermission, + String... permissionType) { + mCurrentPermissionCallback = permissionCallback; + + if (forceShowPermission || !isPermissionAcquired(generatePermissionKeySet(permissionType))) { + requestPermission(permissionType); + return; + } + + mCurrentPermissionCallback.onResult(SHealthConstant.PERMISSION_GRANTED); + } + + public void authorize(OnPermissionCallback permissionCallback) { + authorize(permissionCallback, false); + } + + public void getStepCount(long startTime, long endTime, + @NonNull InputKit.Result callback) { + mStepReader.readStepCount(startTime, endTime, callback); + } + + public void getStepCountDistribution(Options options, int limit, + InputKit.Result callback) { + mStepReader.readStepCountHistories(options, limit, callback); + } + + public void monitorStep(String sensorType, long startTime, + @NonNull HealthProvider.SensorListener listener) { + if (!isPermissionAcquired(mPermissionSet.getStepPermissionSet())) { + listener.onUnsubscribe(new IKResultInfo(IKStatus.Code.S_HEALTH_PERMISSION_REQUIRED, + IKStatus.INPUT_KIT_REQUIRED_GRANTED_PERMISSIONS)); + return; + } + + mStepReader.monitorStepData(sensorType, startTime, listener); + } + + public void stopMonitorStep(String sensorType, + @NonNull HealthProvider.SensorListener listener) { + mStepReader.stopMonitorStepData(sensorType, listener); + } + + public void getStepDistance(long startTime, long endTime, + InputKit.Result callback) { + if (!isPermissionAcquired(mPermissionSet.getStepPermissionSet())) { + callback.onError(new IKResultInfo(IKStatus.Code.S_HEALTH_PERMISSION_REQUIRED, + IKStatus.INPUT_KIT_REQUIRED_GRANTED_PERMISSIONS)); + return; + } + + mStepReader.readStepDistance(startTime, endTime, callback); + } + + public void getStepDistanceSamples(long startTime, long endTime, int limit, + @NonNull InputKit.Result>> callback) { + if (!isPermissionAcquired(mPermissionSet.getStepPermissionSet())) { + callback.onError(new IKResultInfo(IKStatus.Code.S_HEALTH_PERMISSION_REQUIRED, + IKStatus.INPUT_KIT_REQUIRED_GRANTED_PERMISSIONS)); + return; + } + + mStepReader.readStepDistanceSamples(startTime, endTime, limit, callback); + } + + public void getSleepAnalysisSamples(long startTime, long endTime, + @NonNull InputKit.Result>> callback) { + if (!isPermissionAcquired(mPermissionSet.getSleepPermissionSet())) { + callback.onError(new IKResultInfo(IKStatus.Code.S_HEALTH_PERMISSION_REQUIRED, + IKStatus.INPUT_KIT_REQUIRED_GRANTED_PERMISSIONS)); + return; + } + + mSleepReader.readSleep(startTime, endTime, callback); + } + + public void getBloodPressure(long startTime, long endTime, + @NonNull InputKit.Result> callback) { + if (!isPermissionAcquired(mPermissionSet.getBloodPressurePermissionSet())) { + callback.onError(new IKResultInfo(IKStatus.Code.S_HEALTH_PERMISSION_REQUIRED, + IKStatus.INPUT_KIT_REQUIRED_GRANTED_PERMISSIONS)); + return; + } + + mBloodPressureReader.readBloodPressure(startTime, endTime, callback); + } + + public void getWeight(long startTime, long endTime, + @NonNull InputKit.Result> callback) { + if (!isPermissionAcquired(mPermissionSet.getWeightPermissionSet())) { + callback.onError(new IKResultInfo(IKStatus.Code.S_HEALTH_PERMISSION_REQUIRED, + IKStatus.INPUT_KIT_REQUIRED_GRANTED_PERMISSIONS)); + return; + } + + mWeightReader.readWeight(startTime, endTime, callback); + } + + public void getListDataType(@NonNull InputKit.Result> callback) { + callback.onNewData(SHealthConstant.SUPPORTED_DATA_TYPES); + } + + private void initReporter() { + mStepReader = new StepCountReader(mStore); + mSleepReader = new SleepReader(mStore); + mBloodPressureReader = new BloodPressureReader(mStore); + mWeightReader = new WeightReader(mStore); + } + + public boolean isPermissionAcquired(Set sets) { + if (sets == null || sets.size() == 0) { + return false; + } + + HealthPermissionManager pmsManager = new HealthPermissionManager(mStore); + try { + Map resultMap = pmsManager.isPermissionAcquired(sets); + return !resultMap.values().contains(Boolean.FALSE); + } catch (Exception e) { + Log.e(TAG, "Permission request fails.", e); + } + return false; + } + + private void requestPermission(String... permissionType) { + HealthPermissionManager pmsManager = new HealthPermissionManager(mStore); + try { + pmsManager.requestPermissions( + generatePermissionKeySet(permissionType), + null + ).setResultListener(mPermissionListener); + } catch (Exception e) { + Log.e(TAG, "Permission setting fails.", e); + mCurrentPermissionCallback.onResult(SHealthConstant.PERMISSION_DENIED); + } + } + + public Set generatePermissionKeySet(String... permissionType) { + if (permissionType == null || permissionType.length == 0) { + return Collections.emptySet(); + } + Set pmsKeySet = new HashSet<>(); + for (String permission : permissionType) { + if (permission.equals(SampleType.STEP_COUNT) || permission.equals(SampleType.DISTANCE_WALKING_RUNNING)) { + pmsKeySet.addAll(mPermissionSet.getStepPermissionSet()); + } else if (permission.equals(SampleType.SLEEP)) { + pmsKeySet.addAll(mPermissionSet.getSleepPermissionSet()); + } else if (permission.equals(SampleType.WEIGHT)) { + pmsKeySet.addAll(mPermissionSet.getWeightPermissionSet()); + } else if (permission.equals(SampleType.BLOOD_PRESSURE)) { + pmsKeySet.addAll(mPermissionSet.getBloodPressurePermissionSet()); + } + } + return pmsKeySet; + } + + public interface OnConnectCallback { + void onResult(int statusCode, HealthConnectionErrorResult errorInfo); + } + + public interface OnPermissionCallback { + void onResult(int resultCode); + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/shealth/SamsungHealthProvider.java b/android/src/main/java/nl/sense/rninputkit/inputkit/shealth/SamsungHealthProvider.java new file mode 100644 index 0000000..2f44854 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/shealth/SamsungHealthProvider.java @@ -0,0 +1,253 @@ +package nl.sense.rninputkit.inputkit.shealth; + +import android.app.Activity; +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import android.util.Pair; + +import com.samsung.android.sdk.healthdata.HealthConnectionErrorResult; +import com.samsung.android.sdk.healthdata.HealthPermissionManager; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import nl.sense.rninputkit.inputkit.HealthProvider; +import nl.sense.rninputkit.inputkit.HealthProvider; +import nl.sense.rninputkit.inputkit.InputKit; +import nl.sense.rninputkit.inputkit.constant.IKStatus; +import nl.sense.rninputkit.inputkit.constant.Interval; +import nl.sense.rninputkit.inputkit.constant.SampleType; +import nl.sense.rninputkit.inputkit.entity.BloodPressure; +import nl.sense.rninputkit.inputkit.entity.IKValue; +import nl.sense.rninputkit.inputkit.entity.SensorDataPoint; +import nl.sense.rninputkit.inputkit.entity.StepContent; +import nl.sense.rninputkit.inputkit.entity.TimeInterval; +import nl.sense.rninputkit.inputkit.entity.Weight; +import nl.sense.rninputkit.inputkit.Options; +import nl.sense.rninputkit.inputkit.helper.InputKitTimeUtils; +import nl.sense.rninputkit.inputkit.shealth.utils.SHealthUtils; +import nl.sense.rninputkit.inputkit.status.IKProviderInfo; +import nl.sense.rninputkit.inputkit.status.IKResultInfo; + +/** + * Created by xedi on 10/18/17. + */ + +public class SamsungHealthProvider extends HealthProvider { + private static final IKResultInfo REQUIRED_PERMISSION = new IKResultInfo( + IKStatus.Code.S_HEALTH_PERMISSION_REQUIRED, + IKStatus.INPUT_KIT_REQUIRED_GRANTED_PERMISSIONS + ); + private static final IKResultInfo DISCONNECTED = new IKResultInfo( + IKStatus.Code.S_HEALTH_DISCONNECTED, + IKStatus.INPUT_KIT_DISCONNECTED); + private static final IKResultInfo UNSUPPORTED_REALTIME_TRACKING = new IKResultInfo( + IKStatus.Code.INVALID_REQUEST, + "Sample type is not supported for real time monitoring"); + + private SHealthWrapper mSHealthWrapper; + + public SamsungHealthProvider(@NonNull Context context) { + super(context); + mSHealthWrapper = new SHealthWrapper(context); + } + + @Nullable + @Override + public Context getContext() { + return super.getContext(); + } + + @Nullable + @Override + public Activity getHostActivity() { + return super.getHostActivity(); + } + + @Override + public void setHostActivity(@Nullable Activity activity) { + super.setHostActivity(activity); + } + + @Override + protected boolean isAvailable(@NonNull final InputKit.Result callback) { + return super.isAvailable(callback); + } + + @Override + public boolean isAvailable() { + return (mSHealthWrapper.getConnectionStatus() == SHealthConstant.STATUS_CONNECTED); + } + + @Override + public boolean isPermissionsAuthorised(String[] permissionTypes) { + if (permissionTypes == null || permissionTypes.length == 0) { + return false; + } + + Set permissionSet = + mSHealthWrapper.generatePermissionKeySet(permissionTypes); + return mSHealthWrapper.isPermissionAcquired(permissionSet); + } + + @Override + public void authorize(@NonNull final InputKit.Callback callback, final String... permissionType) { + mSHealthWrapper.connectService(new SHealthWrapper.OnConnectCallback() { + @Override + public void onResult(int statusCode, HealthConnectionErrorResult errorInfo) { + if (statusCode == SHealthConstant.STATUS_CONNECTED) { + checkPermissions(callback, permissionType); + } else if (statusCode == SHealthConstant.STATUS_DISCONNECTED) { + callback.onNotAvailable(DISCONNECTED); + } else if (statusCode == SHealthConstant.STATUS_ERROR) { + int errorCode = IKStatus.Code.S_HEALTH_CONNECTION_ERROR; + String msg = IKStatus.INPUT_KIT_CONNECTION_ERROR; + if (errorInfo != null) { + errorCode = errorInfo.getErrorCode(); + msg = getErrorMessage(errorCode); + } + callback.onConnectionRefused(new IKProviderInfo(errorCode, msg)); + } + } + }); + } + + @Override + public void disconnect(@NonNull InputKit.Result callback) { + mSHealthWrapper.disconnect(); + } + + @Override + public void getDistance(long startTime, long endTime, int limit, @NonNull InputKit.Result callback) { + mSHealthWrapper.getStepDistance(adjustTimeToUTC(startTime), adjustTimeToUTC(endTime), callback); + } + + @Override + public void getDistanceSamples(long startTime, long endTime, int limit, + @NonNull InputKit.Result>> callback) { + mSHealthWrapper.getStepDistanceSamples(adjustTimeToUTC(startTime), adjustTimeToUTC(endTime), limit, callback); + } + + @Override + public void getStepCount(@NonNull InputKit.Result callback) { + long startTime = InputKitTimeUtils.getTodayStartTime(); + long endTime = startTime + InputKitTimeUtils.ONE_DAY; + mSHealthWrapper.getStepCount(startTime, endTime, callback); + } + + @Override + public void getStepCount(long startTime, long endTime, int limit, + @NonNull InputKit.Result callback) { + mSHealthWrapper.getStepCount(adjustTimeToUTC(startTime), adjustTimeToUTC(endTime), callback); + } + + @Override + public void getStepCountDistribution(long startTime, long endTime, + @NonNull @Interval.IntervalName String interval, + int limit, @NonNull InputKit.Result callback) { + TimeInterval timeInterval = new TimeInterval(interval); + Options options = new Options.Builder() + .startTime(adjustTimeToUTC(startTime)) + .endTime(adjustTimeToUTC(endTime)) + .timeInterval(timeInterval) + .useDataAggregation() + .build(); + mSHealthWrapper.getStepCountDistribution(options, limit, callback); + } + + @Override + public void getSleepAnalysisSamples(long startTime, long endTime, + @NonNull InputKit.Result>> callback) { + mSHealthWrapper.getSleepAnalysisSamples(adjustTimeToUTC(startTime), adjustTimeToUTC(endTime), callback); + } + + @Override + public void getBloodPressure(long startTime, long endTime, + @NonNull InputKit.Result> callback) { + mSHealthWrapper.getBloodPressure(adjustTimeToUTC(startTime), adjustTimeToUTC(endTime), callback); + } + + @Override + public void getWeight(long startTime, long endTime, + @NonNull InputKit.Result> callback) { + mSHealthWrapper.getWeight(adjustTimeToUTC(startTime), adjustTimeToUTC(endTime), callback); + } + + public void getListDataType(@NonNull InputKit.Result> callback) { + mSHealthWrapper.getListDataType(callback); + } + + @Override + public void startMonitoring(@NonNull @SampleType.SampleName String sensorType, + @NonNull Pair samplingRate, + @NonNull SensorListener listener) { + if (sensorType.equals(SampleType.STEP_COUNT)) { + long startTime = System.currentTimeMillis(); + mSHealthWrapper.monitorStep(sensorType, adjustTimeToUTC(startTime), listener); + } else listener.onSubscribe(UNSUPPORTED_REALTIME_TRACKING); + } + + @Override + public void stopMonitoring(@NonNull @SampleType.SampleName String sensorType, + @NonNull SensorListener listener) { + if (sensorType.equals(SampleType.STEP_COUNT)) { + mSHealthWrapper.stopMonitorStep(sensorType, listener); + } else listener.onSubscribe(UNSUPPORTED_REALTIME_TRACKING); + } + + @Override + public void startTracking(@NonNull @SampleType.SampleName String sensorType, + @NonNull Pair samplingRate, + @NonNull SensorListener listener) { + if (sensorType.equals(SampleType.STEP_COUNT)) { + long startTime = System.currentTimeMillis(); + mSHealthWrapper.monitorStep(sensorType, adjustTimeToUTC(startTime), listener); + } else listener.onSubscribe(UNSUPPORTED_REALTIME_TRACKING); + } + + @Override + public void stopTracking(@NonNull String sensorType, + @NonNull SensorListener listener) { + if (sensorType.equals(SampleType.STEP_COUNT)) { + mSHealthWrapper.stopMonitorStep(sensorType, listener); + } else listener.onSubscribe(UNSUPPORTED_REALTIME_TRACKING); + } + + @Override + public void stopTrackingAll(@NonNull SensorListener listener) { + mSHealthWrapper.stopMonitorStep("", listener); + } + + private void checkPermissions(@NonNull final InputKit.Callback callback, String... permissionType) { + mSHealthWrapper.authorize(new SHealthWrapper.OnPermissionCallback() { + @Override + public void onResult(int resultCode) { + if (resultCode == SHealthConstant.STATUS_CONNECTED) { + callback.onAvailable(); + } else if (resultCode == SHealthConstant.STATUS_DISCONNECTED) { + callback.onNotAvailable(REQUIRED_PERMISSION); + } + } + }, false, permissionType); + } + + private String getErrorMessage(int errorCode) { + switch (errorCode) { + case HealthConnectionErrorResult.PLATFORM_NOT_INSTALLED: + return IKStatus.SAMSUNG_HEALTH_NOT_INSTALLED; + case HealthConnectionErrorResult.OLD_VERSION_PLATFORM: + return IKStatus.SAMSUNG_HEALTH_OLD_VERSION; + case HealthConnectionErrorResult.PLATFORM_DISABLED: + return IKStatus.SAMSUNG_HEALTH_DISABLED; + case HealthConnectionErrorResult.USER_AGREEMENT_NEEDED: + return IKStatus.SAMSUNG_HEALTH_USER_AGREEMENT_NEEDED; + default: return IKStatus.SAMSUNG_HEALTH_IS_NOT_AVAILABLE; + } + } + + private long adjustTimeToUTC(long time) { + return time + SHealthUtils.timeDiff(); + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/shealth/SleepReader.java b/android/src/main/java/nl/sense/rninputkit/inputkit/shealth/SleepReader.java new file mode 100644 index 0000000..9bfd5c9 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/shealth/SleepReader.java @@ -0,0 +1,73 @@ +package nl.sense.rninputkit.inputkit.shealth; + +import androidx.annotation.NonNull; + +import com.samsung.android.sdk.healthdata.HealthConstants; +import com.samsung.android.sdk.healthdata.HealthData; +import com.samsung.android.sdk.healthdata.HealthDataResolver; +import com.samsung.android.sdk.healthdata.HealthDataStore; +import com.samsung.android.sdk.healthdata.HealthResultHolder; + +import java.util.ArrayList; +import java.util.List; + +import nl.sense.rninputkit.inputkit.InputKit; +import nl.sense.rninputkit.inputkit.constant.IKStatus; +import nl.sense.rninputkit.inputkit.entity.DateContent; +import nl.sense.rninputkit.inputkit.entity.IKValue; +import nl.sense.rninputkit.inputkit.status.IKResultInfo; + +/** + * Created by xedi on 10/4/17. + */ + +public class SleepReader { + private final HealthDataResolver mResolver; + + public SleepReader(HealthDataStore store) { + mResolver = new HealthDataResolver(store, null); + } + + public void readSleep(final long startTime, final long stopTime, + @NonNull final InputKit.Result>> callback) { + try { + HealthDataResolver.ReadRequest request = createRequest(startTime, stopTime); + mResolver.read(request).setResultListener( + new HealthResultHolder.ResultListener() { + @Override + public void onResult(HealthDataResolver.ReadResult healthDatas) { + List> sleepData = new ArrayList>(); + try { + for (HealthData data : healthDatas) { + Long goBed = data.getLong(HealthConstants.Sleep.START_TIME); + Long wakeUp = data.getLong(HealthConstants.Sleep.END_TIME); + IKValue sleep = new IKValue<>(SHealthConstant.ASLEEP, + new DateContent(goBed), + new DateContent(wakeUp)); + sleepData.add(sleep); + } + } finally { + callback.onNewData(sleepData); + healthDatas.close(); + } + } + }); + } catch (Exception e) { + callback.onError(new IKResultInfo(IKStatus.Code.UNKNOWN_ERROR, e.getMessage())); + } + } + + private HealthDataResolver.ReadRequest createRequest(long startTime, long stopTime) { + return new HealthDataResolver.ReadRequest.Builder() + .setDataType(HealthConstants.Sleep.HEALTH_DATA_TYPE) + .setProperties(new String[]{ + HealthConstants.Sleep.DEVICE_UUID, + HealthConstants.Sleep.START_TIME, + HealthConstants.Sleep.END_TIME, + HealthConstants.Sleep.PACKAGE_NAME}) + .setLocalTimeRange(HealthConstants.Sleep.START_TIME, + HealthConstants.Sleep.TIME_OFFSET, startTime, stopTime) + .setSort(HealthConstants.Sleep.START_TIME, HealthDataResolver.SortOrder.DESC) + .build(); + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/shealth/StepBinningData.java b/android/src/main/java/nl/sense/rninputkit/inputkit/shealth/StepBinningData.java new file mode 100644 index 0000000..9f76768 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/shealth/StepBinningData.java @@ -0,0 +1,17 @@ +package nl.sense.rninputkit.inputkit.shealth; + +/** + * Created by xedi on 11/16/17. + */ + +public class StepBinningData { + public final int count; + public final float distance; + public String time; + + public StepBinningData(String time, int count, float distance) { + this.time = time; + this.count = count; + this.distance = distance; + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/shealth/StepCountReader.java b/android/src/main/java/nl/sense/rninputkit/inputkit/shealth/StepCountReader.java new file mode 100755 index 0000000..df7abb0 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/shealth/StepCountReader.java @@ -0,0 +1,307 @@ +/** + * Copyright (C) Sense Health BV + * modified from s-health sample + */ + +package nl.sense.rninputkit.inputkit.shealth; + + +import androidx.annotation.NonNull; +import android.util.Pair; + +import com.samsung.android.sdk.healthdata.HealthConstants; +import com.samsung.android.sdk.healthdata.HealthData; +import com.samsung.android.sdk.healthdata.HealthDataResolver; +import com.samsung.android.sdk.healthdata.HealthDataResolver.AggregateRequest; +import com.samsung.android.sdk.healthdata.HealthDataResolver.AggregateRequest.AggregateFunction; +import com.samsung.android.sdk.healthdata.HealthDataResolver.AggregateRequest.TimeGroupUnit; +import com.samsung.android.sdk.healthdata.HealthDataResolver.Filter; +import com.samsung.android.sdk.healthdata.HealthDataResolver.SortOrder; +import com.samsung.android.sdk.healthdata.HealthDataStore; +import com.samsung.android.sdk.healthdata.HealthResultHolder; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import nl.sense.rninputkit.inputkit.HealthProvider; +import nl.sense.rninputkit.inputkit.InputKit; +import nl.sense.rninputkit.inputkit.constant.IKStatus; +import nl.sense.rninputkit.inputkit.constant.Interval; +import nl.sense.rninputkit.inputkit.entity.IKValue; +import nl.sense.rninputkit.inputkit.entity.SensorDataPoint; +import nl.sense.rninputkit.inputkit.entity.StepContent; +import nl.sense.rninputkit.inputkit.entity.TimeInterval; +import nl.sense.rninputkit.inputkit.Options; +import nl.sense.rninputkit.inputkit.shealth.utils.DataMapper; +import nl.sense.rninputkit.inputkit.shealth.utils.SHealthUtils; +import nl.sense.rninputkit.inputkit.status.IKResultInfo; + +public class StepCountReader { + + public static final String TAG = "S_HEALTH"; + public static final String STEP_SUMMARY_DATA_TYPE_NAME = "com.samsung.shealth.step_daily_trend"; + private static final String ALIAS_TOTAL_COUNT = "count"; + private static final String ALIAS_TOTAL_DISTANCE = "distance"; + private static final String ALIAS_DEVICE_UUID = "deviceuuid"; + private static final String ALIAS_BINNING_TIME = "binning_time"; + private final HealthDataResolver mResolver; + private Map mStepMonitorMaps = new HashMap<>(); + + public StepCountReader(HealthDataStore store) { + mResolver = new HealthDataResolver(store, null); + } + + public void readStepCount(final long startTime, final long stopTime, + @NonNull final InputKit.Result callback) { + requestStepData(startTime, stopTime, new StepRequestListener(callback) { + @Override + public void onStepCount(int count) { + callback.onNewData(count); + } + }); + } + + public void readStepCountHistories(Options options, final int limit, + @NonNull final InputKit.Result callback) { + final long startTime = options.getStartTime(); + final long endTime = options.getEndTime(); + final TimeInterval timeInterval = options.getTimeInterval(); + requestStepData(startTime, endTime, new StepRequestListener(callback) { + @Override + void onDeviceId(String deviceId) { + readStepCountHistories(startTime, endTime, limit, timeInterval, deviceId, callback); + } + }); + } + + public void readStepDistance(long startTime, long stopTime, + @NonNull final InputKit.Result callback) { + requestStepData(startTime, stopTime, new StepRequestListener(callback) { + @Override + void onStepDistance(float distance) { + callback.onNewData(distance); + } + }); + } + + public void readStepDistanceSamples(final long startTime, final long stopTime, final int limit, + @NonNull final InputKit.Result>> callback) { + requestStepData(startTime, stopTime, new StepRequestListener(callback) { + @Override + public void onDeviceId(String deviceId) { + TimeInterval timeInterval = new TimeInterval(Interval.ONE_MINUTE); + readStepDistanceHistories(startTime, stopTime, limit, timeInterval, deviceId, callback); + } + }); + } + + private void requestStepData(final long startTime, final long stopTime, + @NonNull final StepRequestListener listener) { + AggregateRequest request = aggregateStep(startTime, stopTime); + try { + mResolver.aggregate(request).setResultListener( + new HealthResultHolder.ResultListener() { + @Override + public void onResult(HealthDataResolver.AggregateResult healthDatas) { + String deviceUuid = null; + int totalCount = 0; + float totalDistance = 0.0f; + try { + Iterator iterator = healthDatas.iterator(); + if (iterator.hasNext()) { + HealthData data = iterator.next(); + deviceUuid = data.getString(ALIAS_DEVICE_UUID); + totalCount = data.getInt(ALIAS_TOTAL_COUNT); + totalDistance = data.getFloat(ALIAS_TOTAL_DISTANCE); + } + } finally { + listener.onStepCount(totalCount); + listener.onStepDistance(totalDistance); + healthDatas.close(); + } + if (deviceUuid != null) { + listener.onDeviceId(deviceUuid); + } else { + listener.onError(new IKResultInfo(IKStatus.Code.UNKNOWN_ERROR, + IKStatus.INPUT_KIT_NO_DEVICES_SOURCE)); + } + } + }); + } catch (Exception e) { + listener.onError(new IKResultInfo(IKStatus.Code.UNKNOWN_ERROR, e.getMessage())); + } + } + + public void monitorStepData(String sensorType, long startTime, + HealthProvider.SensorListener realTimeStepListener) { + if (mStepMonitorMaps.get(sensorType) == null) { + StepMonitor sm = new StepMonitor(startTime, + sensorType, + mResolver, + realTimeStepListener); + mStepMonitorMaps.put(sensorType, sm); + } else { + realTimeStepListener.onSubscribe(new IKResultInfo(IKStatus.Code.VALID_REQUEST, + IKStatus.INPUT_KIT_MONITOR_REGISTERED)); + } + } + + public void stopMonitorStepData(String sensorType, + HealthProvider.SensorListener realTimeStepListener) { + if (sensorType.equals("")) { + Iterator> it = mStepMonitorMaps.entrySet().iterator(); + while (it.hasNext()) { + StepMonitor sm = it.next().getValue(); + sm.stopMonitor(); + } + mStepMonitorMaps.clear(); + } else { + StepMonitor sm = mStepMonitorMaps.get(sensorType); + if (sm == null) { + realTimeStepListener.onSubscribe(new IKResultInfo(IKStatus.Code.VALID_REQUEST, + IKStatus.INPUT_KIT_MONITORING_NOT_AVAILABLE)); + } else { + sm.stopMonitor(); + mStepMonitorMaps.remove(sensorType); + realTimeStepListener.onSubscribe(new IKResultInfo(IKStatus.Code.VALID_REQUEST, + IKStatus.INPUT_KIT_MONITOR_REGISTERED)); + } + } + } + + private AggregateRequest aggregateStep(long startTime, long stopTime) { + return new AggregateRequest.Builder() + .setDataType(HealthConstants.StepCount.HEALTH_DATA_TYPE) + .addFunction(AggregateFunction.SUM, HealthConstants.StepCount.COUNT, ALIAS_TOTAL_COUNT) + .addFunction(AggregateFunction.SUM, HealthConstants.StepCount.DISTANCE, ALIAS_TOTAL_DISTANCE) + .addGroup(HealthConstants.StepCount.DEVICE_UUID, ALIAS_DEVICE_UUID) + .setLocalTimeRange(HealthConstants.StepCount.START_TIME, HealthConstants.StepCount.TIME_OFFSET, + startTime, stopTime) + .setSort(ALIAS_TOTAL_COUNT, SortOrder.DESC) + .build(); + } + + private AggregateRequest aggregateStepHistory(long startTime, long stopTime, + Pair interval, String deviceUuid) { + Filter filter = Filter.eq(HealthConstants.StepCount.DEVICE_UUID, deviceUuid); + return new AggregateRequest.Builder() + .setDataType(HealthConstants.StepCount.HEALTH_DATA_TYPE) + .addFunction(AggregateFunction.SUM, + HealthConstants.StepCount.COUNT, ALIAS_TOTAL_COUNT) + .addFunction(AggregateFunction.SUM, + HealthConstants.StepCount.DISTANCE, ALIAS_TOTAL_DISTANCE) + .setTimeGroup(interval.first, interval.second, + HealthConstants.StepCount.START_TIME, + HealthConstants.StepCount.TIME_OFFSET, + ALIAS_BINNING_TIME) + .setLocalTimeRange(HealthConstants.StepCount.START_TIME, + HealthConstants.StepCount.TIME_OFFSET, startTime, stopTime) + .setFilter(filter) + .setSort(ALIAS_BINNING_TIME, SortOrder.ASC) + .build(); + } + + private void readStepCountHistories(final long startTime, final long stopTime, final int limit, + TimeInterval timeInterval, String deviceUuid, + @NonNull final InputKit.Result callbackStepHistory) { + try { + final Pair ret = SHealthUtils.convertTimeInterval(timeInterval); + AggregateRequest request = aggregateStepHistory(startTime, stopTime, ret, deviceUuid); + mResolver.aggregate(request).setResultListener( + new HealthResultHolder.ResultListener() { + @Override + public void onResult(HealthDataResolver.AggregateResult healthDatas) { + List binningCountArray = new ArrayList<>(); + try { + for (HealthData data : healthDatas) { + String binningTime = data.getString(ALIAS_BINNING_TIME); + int binningCount = data.getInt(ALIAS_TOTAL_COUNT); + float binningDistance = data.getInt(ALIAS_TOTAL_DISTANCE); + + if (binningTime != null) { + binningCountArray.add(new StepBinningData(binningTime, + binningCount, + binningDistance)); + } + } + StepContent stepContent = DataMapper.convertStepCount(startTime, + stopTime, limit, ret, + binningCountArray); + callbackStepHistory.onNewData(stepContent); + } finally { + healthDatas.close(); + } + } + }); + } catch (Exception e) { + callbackStepHistory.onError(new IKResultInfo(IKStatus.Code.UNKNOWN_ERROR, + e.getMessage())); + } + } + + private void readStepDistanceHistories(final long startTime, final long stopTime, + final int limit, + TimeInterval timeInterval, String deviceUuid, + @NonNull final InputKit.Result>> callback) { + try { + final Pair ret = + SHealthUtils.convertTimeInterval(timeInterval); + AggregateRequest request = aggregateStepHistory(startTime, + stopTime, ret, + deviceUuid); + mResolver.aggregate(request).setResultListener( + new HealthResultHolder.ResultListener() { + @Override + public void onResult(HealthDataResolver.AggregateResult healthDatas) { + List binningCountArray = new ArrayList<>(); + List> distanceList = new ArrayList>(); + try { + for (HealthData data : healthDatas) { + String binningTime = data.getString(ALIAS_BINNING_TIME); + int binningCount = data.getInt(ALIAS_TOTAL_COUNT); + float binningDistance = data.getInt(ALIAS_TOTAL_DISTANCE); + + if (binningTime != null) { + binningCountArray.add(new StepBinningData(binningTime, + binningCount, + binningDistance)); + } + } + distanceList = DataMapper.convertStepDistance(ret, + limit, + binningCountArray); + } finally { + callback.onNewData(distanceList); + healthDatas.close(); + } + } + }); + } catch (Exception e) { + callback.onError(new IKResultInfo(IKStatus.Code.UNKNOWN_ERROR, e.getMessage())); + } + } + + private abstract class StepRequestListener { + final InputKit.Result resultCallback; + + StepRequestListener(InputKit.Result callback) { + this.resultCallback = callback; + } + + void onStepCount(int count) { + } + + void onStepDistance(float distance) { + } + + void onDeviceId(String deviceId) { + } + + void onError(IKResultInfo error) { + resultCallback.onError(error); + } + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/shealth/StepMonitor.java b/android/src/main/java/nl/sense/rninputkit/inputkit/shealth/StepMonitor.java new file mode 100644 index 0000000..3a7cebd --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/shealth/StepMonitor.java @@ -0,0 +1,131 @@ +package nl.sense.rninputkit.inputkit.shealth; + +import android.os.Handler; +import android.os.Looper; + +import com.samsung.android.sdk.healthdata.HealthConstants; +import com.samsung.android.sdk.healthdata.HealthData; +import com.samsung.android.sdk.healthdata.HealthDataResolver; +import com.samsung.android.sdk.healthdata.HealthResultHolder; + +import java.util.Iterator; +import java.util.concurrent.TimeUnit; + +import nl.sense.rninputkit.inputkit.HealthProvider; +import nl.sense.rninputkit.inputkit.constant.IKStatus; +import nl.sense.rninputkit.inputkit.entity.SensorDataPoint; +import nl.sense.rninputkit.inputkit.shealth.utils.DataMapper; +import nl.sense.rninputkit.inputkit.status.IKResultInfo; + +/** + * Created by xedi on 11/7/17. + */ + +public class StepMonitor { + private static final String ALIAS_TOTAL_COUNT = "count"; + private static final String ALIAS_TOTAL_DISTANCE = "distance"; + private static final String ALIAS_DEVICE_UUID = "deviceuuid"; + + private static final int PERIOD_IN_MS = 15 * 1000; + private long mStartTime; + private HealthProvider.SensorListener mRealTimeStepListener; + private Handler mHandler = null; + private HealthDataResolver mResolver = null; + private String mSensorType; + private boolean isStopped = false; + private Runnable mTaskRunnable = new Runnable() { + @Override + public void run() { + if (!isStopped) { + executeMonitoring(); + mHandler.postDelayed(this, TimeUnit.MILLISECONDS.toMillis(PERIOD_IN_MS)); + } + } + }; + private HealthResultHolder.ResultListener mStepListener = + new HealthResultHolder.ResultListener() { + @Override + public void onResult(HealthDataResolver.AggregateResult healthDatas) { + String deviceUuid = null; + int totalCount = 0; + float totalDistance = 0.0f; + try { + Iterator iterator = healthDatas.iterator(); + if (iterator.hasNext()) { + HealthData data = iterator.next(); + deviceUuid = data.getString(ALIAS_DEVICE_UUID); + totalCount = data.getInt(ALIAS_TOTAL_COUNT); + totalDistance = data.getFloat(ALIAS_TOTAL_DISTANCE); + } + } finally { + healthDatas.close(); + } + if (mRealTimeStepListener != null) { + mRealTimeStepListener.onReceive( + DataMapper.toSensorDataPoint(mSensorType, + mStartTime, totalCount, + totalDistance)); + monitorStepDataTask(); + } + } + }; + + StepMonitor(long startTime, String sensorType, HealthDataResolver resolver, + HealthProvider.SensorListener listener) { + mStartTime = startTime; + mResolver = resolver; + mRealTimeStepListener = listener; + mSensorType = sensorType; + executeMonitoring(); + } + + private void executeMonitoring() { + long stopTime = mStartTime + SHealthConstant.ONE_DAY; + HealthDataResolver.AggregateRequest request = aggregateStep(mStartTime, stopTime); + try { + mResolver.aggregate(request).setResultListener(mStepListener); + } catch (Exception e) { + mRealTimeStepListener.onUnsubscribe( + new IKResultInfo(IKStatus.Code.UNKNOWN_ERROR, e.getMessage())); + stopMonitor(false); + } + } + + public void stopMonitor(boolean callCallback) { + if (mHandler != null) { + mHandler.removeCallbacks(mTaskRunnable); + if (callCallback && !isStopped) { + mRealTimeStepListener.onUnsubscribe( + new IKResultInfo(IKStatus.Code.VALID_REQUEST, + IKStatus.INPUT_KIT_MONITOR_UNREGISTERED)); + } + } + isStopped = true; + } + + public void stopMonitor() { + stopMonitor(true); + } + + private void monitorStepDataTask() { + if (mHandler == null) { + mHandler = new Handler(Looper.getMainLooper()); + mHandler.postDelayed(mTaskRunnable, TimeUnit.MILLISECONDS.toMillis(PERIOD_IN_MS)); + } + } + + private HealthDataResolver.AggregateRequest aggregateStep(long startTime, long stopTime) { + return new HealthDataResolver.AggregateRequest.Builder() + .setDataType(HealthConstants.StepCount.HEALTH_DATA_TYPE) + .addFunction(HealthDataResolver.AggregateRequest.AggregateFunction.SUM, + HealthConstants.StepCount.COUNT, ALIAS_TOTAL_COUNT) + .addFunction(HealthDataResolver.AggregateRequest.AggregateFunction.SUM, + HealthConstants.StepCount.DISTANCE, ALIAS_TOTAL_DISTANCE) + .addGroup(HealthConstants.StepCount.DEVICE_UUID, ALIAS_DEVICE_UUID) + .setLocalTimeRange(HealthConstants.StepCount.START_TIME, + HealthConstants.StepCount.TIME_OFFSET, + startTime, stopTime) + .setSort(ALIAS_TOTAL_COUNT, HealthDataResolver.SortOrder.DESC) + .build(); + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/shealth/WeightReader.java b/android/src/main/java/nl/sense/rninputkit/inputkit/shealth/WeightReader.java new file mode 100644 index 0000000..c230d42 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/shealth/WeightReader.java @@ -0,0 +1,78 @@ +package nl.sense.rninputkit.inputkit.shealth; + +import androidx.annotation.NonNull; + +import com.samsung.android.sdk.healthdata.HealthConstants; +import com.samsung.android.sdk.healthdata.HealthData; +import com.samsung.android.sdk.healthdata.HealthDataResolver; +import com.samsung.android.sdk.healthdata.HealthDataStore; +import com.samsung.android.sdk.healthdata.HealthResultHolder; + +import java.util.ArrayList; +import java.util.List; + +import nl.sense.rninputkit.inputkit.InputKit; +import nl.sense.rninputkit.inputkit.constant.IKStatus; +import nl.sense.rninputkit.inputkit.entity.Weight; +import nl.sense.rninputkit.inputkit.status.IKResultInfo; + +/** + * Created by xedi on 10/9/17. + */ + +public class WeightReader { + private final HealthDataResolver mResolver; + + public WeightReader(HealthDataStore store) { + mResolver = new HealthDataResolver(store, null); + } + + public void readWeight(final long startTime, final long stopTime, + @NonNull final InputKit.Result> callback) { + try { + HealthDataResolver.ReadRequest request = createRequest(startTime, stopTime); + mResolver.read(request).setResultListener( + new HealthResultHolder.ResultListener() { + @Override + public void onResult(HealthDataResolver.ReadResult healthDatas) { + List weightList = new ArrayList(); + try { + for (HealthData data : healthDatas) { + Long time = data.getLong(HealthConstants.Weight.START_TIME); + Float weight = data.getFloat(HealthConstants.Weight.WEIGHT); + Integer bodyFat = data.getInt(HealthConstants.Weight.BODY_FAT); + String comment = data.getString(HealthConstants.Weight.COMMENT); + + Weight weightData = new Weight(weight, bodyFat, time); + weightData.setComment(comment); + weightList.add(weightData); + } + } finally { + callback.onNewData(weightList); + healthDatas.close(); + } + } + }); + } catch (Exception e) { + callback.onError( + new IKResultInfo(IKStatus.Code.UNKNOWN_ERROR, + e.getMessage())); + } + } + + private HealthDataResolver.ReadRequest createRequest(long startTime, long stopTime) { + return new HealthDataResolver.ReadRequest.Builder() + .setDataType(HealthConstants.Weight.HEALTH_DATA_TYPE) + .setProperties(new String[]{ + HealthConstants.Weight.DEVICE_UUID, + HealthConstants.Weight.START_TIME, + HealthConstants.Weight.WEIGHT, + HealthConstants.Weight.BODY_FAT, + HealthConstants.Weight.COMMENT}) + .setLocalTimeRange(HealthConstants.Weight.START_TIME, + HealthConstants.Weight.TIME_OFFSET, startTime, stopTime) + .setSort(HealthConstants.Weight.START_TIME, + HealthDataResolver.SortOrder.DESC) + .build(); + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/shealth/utils/DataMapper.java b/android/src/main/java/nl/sense/rninputkit/inputkit/shealth/utils/DataMapper.java new file mode 100644 index 0000000..989147a --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/shealth/utils/DataMapper.java @@ -0,0 +1,144 @@ +package nl.sense.rninputkit.inputkit.shealth.utils; + +import android.util.Pair; + +import com.samsung.android.sdk.healthdata.HealthDataResolver; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import nl.sense.rninputkit.inputkit.constant.SampleType; +import nl.sense.rninputkit.inputkit.entity.DateContent; +import nl.sense.rninputkit.inputkit.entity.IKValue; +import nl.sense.rninputkit.inputkit.entity.SensorDataPoint; +import nl.sense.rninputkit.inputkit.entity.Step; +import nl.sense.rninputkit.inputkit.entity.StepContent; +import nl.sense.rninputkit.inputkit.shealth.StepBinningData; + +/** + * Created by xedi on 10/19/17. + */ + +public final class DataMapper { + + public static StepContent convertStepCount(long startTime, long endTime, int limit, + Pair interval, + List stepBinningDataList) { + long timeDiff = SHealthUtils.timeDiff(); + long startTimeSrcLocal = startTime - timeDiff; + long endTimeSrcLocal = endTime - timeDiff; + long intervalInMillis = SHealthUtils.intervalToMillis(interval); + + List contents = new ArrayList<>(); + long firstDataTime = startTime; + if (!stepBinningDataList.isEmpty()) { + StepBinningData firstData = stepBinningDataList.get(0); + firstDataTime = SHealthUtils.toMillis(firstData.time, interval.first); + } + // to bottom range + long pivotTimeBott = firstDataTime; + do { + contents.add( + createStep(stepBinningDataList, + interval.first, + pivotTimeBott, + pivotTimeBott + intervalInMillis)); + pivotTimeBott = pivotTimeBott - intervalInMillis; + } while (pivotTimeBott > startTimeSrcLocal); + contents.add( + createStep(stepBinningDataList, + interval.first, + pivotTimeBott, + pivotTimeBott + intervalInMillis)); + Collections.reverse(contents); + // to top range + do { + firstDataTime = firstDataTime + intervalInMillis; + contents.add( + createStep(stepBinningDataList, + interval.first, + firstDataTime, + firstDataTime + intervalInMillis)); + } while ((firstDataTime + intervalInMillis) < endTimeSrcLocal); + + List contentLimits = new ArrayList<>(); + for (Step step : contents) { + if (outOfLimit(contentLimits.size(), limit)) { + break; + } + contentLimits.add(step); + } + return new StepContent( + true, + startTimeSrcLocal, + endTimeSrcLocal, + contentLimits + ); + } + + public static List> + convertStepDistance(Pair interval, + int limit, + List stepBinningDataList) { + long intervalInMillis = SHealthUtils.intervalToMillis(interval); + + List> distanceList = new ArrayList<>(); + for (StepBinningData sd : stepBinningDataList) { + if (outOfLimit(distanceList.size(), limit)) { + break; + } + float distanceValue = sd.distance; + long time = SHealthUtils.toMillis(sd.time, interval.first); + IKValue distance = new IKValue(distanceValue, + new DateContent(time), + new DateContent(time + intervalInMillis)); + distanceList.add(distance); + } + return distanceList; + } + + public static SensorDataPoint toSensorDataPoint(String type, + long startTime, + int step, float distance) { + List> payloads = new ArrayList<>(); + startTime = startTime - SHealthUtils.timeDiff(); + long endTime = System.currentTimeMillis(); + String topic = ""; + if (type.contains(SampleType.STEP_COUNT)) { + payloads.add(new IKValue(step, + new DateContent(startTime), + new DateContent(endTime))); + topic = SampleType.STEP_COUNT; + } + if (type.contains(SampleType.DISTANCE_WALKING_RUNNING)) { + payloads.add(new IKValue(distance, + new DateContent(startTime), + new DateContent(endTime))); + topic = topic + SampleType.DISTANCE_WALKING_RUNNING; + } + return new SensorDataPoint( + topic, + payloads + ); + } + + private static boolean outOfLimit(int size, int limit) { + return (limit > 0 && size >= limit); + } + + private static Step createStep(List stepBinningDataList, + HealthDataResolver.AggregateRequest.TimeGroupUnit tgu, + long time1, long time2) { + int stepCount = 0; + for (StepBinningData sd : stepBinningDataList) { + long time = SHealthUtils.toMillis(sd.time, tgu); + if (time1 == time) { + stepCount = sd.count; + break; + } + } + return new Step(stepCount, time1, time2); + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/shealth/utils/SHealthPermissionSet.java b/android/src/main/java/nl/sense/rninputkit/inputkit/shealth/utils/SHealthPermissionSet.java new file mode 100644 index 0000000..360262d --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/shealth/utils/SHealthPermissionSet.java @@ -0,0 +1,65 @@ +package nl.sense.rninputkit.inputkit.shealth.utils; + +import com.samsung.android.sdk.healthdata.HealthConstants; +import com.samsung.android.sdk.healthdata.HealthPermissionManager; + +import java.util.HashSet; +import java.util.Set; + +import nl.sense.rninputkit.inputkit.shealth.StepCountReader; + +public class SHealthPermissionSet { + private static SHealthPermissionSet sHealthPermissionSet; + private final Set stepPermissionSet; + private final Set sleepPermissionSet; + private final Set weightPermissionSet; + private final Set bloodPressurePermissionSet; + + SHealthPermissionSet() { + stepPermissionSet = createPermissionSet( + new String[]{ + HealthConstants.StepCount.HEALTH_DATA_TYPE, + StepCountReader.STEP_SUMMARY_DATA_TYPE_NAME + }); + sleepPermissionSet = createPermissionSet( + new String[]{HealthConstants.Sleep.HEALTH_DATA_TYPE}); + weightPermissionSet = createPermissionSet( + new String[]{HealthConstants.Weight.HEALTH_DATA_TYPE}); + bloodPressurePermissionSet = createPermissionSet( + new String[]{HealthConstants.BloodPressure.HEALTH_DATA_TYPE}); + } + + public static SHealthPermissionSet getInstance() { + if (sHealthPermissionSet == null) { + sHealthPermissionSet = new SHealthPermissionSet(); + } + return sHealthPermissionSet; + } + + public Set getStepPermissionSet() { + return stepPermissionSet; + } + + public Set getSleepPermissionSet() { + return sleepPermissionSet; + } + + public Set getWeightPermissionSet() { + return weightPermissionSet; + } + + public Set getBloodPressurePermissionSet() { + return bloodPressurePermissionSet; + } + + private Set createPermissionSet(String[] dataTypes) { + Set pmsKeySet = new HashSet<>(); + for (String permissionKey : dataTypes) { + pmsKeySet.add(new HealthPermissionManager.PermissionKey( + permissionKey, + HealthPermissionManager.PermissionType.READ) + ); + } + return pmsKeySet; + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/shealth/utils/SHealthUtils.java b/android/src/main/java/nl/sense/rninputkit/inputkit/shealth/utils/SHealthUtils.java new file mode 100644 index 0000000..e1dc7df --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/shealth/utils/SHealthUtils.java @@ -0,0 +1,78 @@ +package nl.sense.rninputkit.inputkit.shealth.utils; + +import android.util.Pair; + +import com.samsung.android.sdk.healthdata.HealthDataResolver; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Locale; +import java.util.TimeZone; +import java.util.concurrent.TimeUnit; + +import nl.sense.rninputkit.inputkit.entity.TimeInterval; +import nl.sense.rninputkit.inputkit.shealth.SHealthConstant; + +/** + * Created by xedi on 10/19/17. + */ + +public final class SHealthUtils { + + public static long toMillis(String dTime, HealthDataResolver.AggregateRequest.TimeGroupUnit timeGroupUnit) { + SimpleDateFormat sdf; + String format = SHealthConstant.DATE_FORMAT_MINUTELY; + if (timeGroupUnit.equals(HealthDataResolver.AggregateRequest.TimeGroupUnit.DAILY)) { + format = SHealthConstant.DATE_FORMAT_DAILY; + } else if (timeGroupUnit.equals(HealthDataResolver.AggregateRequest.TimeGroupUnit.HOURLY)) { + format = SHealthConstant.DATE_FORMAT_HOURLY; + } else if (timeGroupUnit.equals(HealthDataResolver.AggregateRequest.TimeGroupUnit.MINUTELY)) { + format = SHealthConstant.DATE_FORMAT_MINUTELY; + } + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + sdf = new SimpleDateFormat(format, Locale.getDefault()); + try { + cal.setTime(sdf.parse(dTime)); + } catch (ParseException e) { + e.printStackTrace(); + } + return cal.getTimeInMillis(); + } + + public static long timeDiff() { + Calendar cal = Calendar.getInstance(); + return cal.get(Calendar.ZONE_OFFSET); + } + + public static long intervalToMillis(Pair interval) { + HealthDataResolver.AggregateRequest.TimeGroupUnit unit = interval.first; + Integer value = interval.second; + if (unit.equals(HealthDataResolver.AggregateRequest.TimeGroupUnit.DAILY)) { + return SHealthConstant.ONE_DAY * value; + } else if (unit.equals(HealthDataResolver.AggregateRequest.TimeGroupUnit.HOURLY)) { + return SHealthConstant.ONE_HOUR * value; + } else if (unit.equals(HealthDataResolver.AggregateRequest.TimeGroupUnit.MINUTELY)) { + return SHealthConstant.ONE_MINUTE * value; + } + return SHealthConstant.ONE_MINUTE * value; + } + + + public static Pair convertTimeInterval(TimeInterval timeInterval) { + if (timeInterval.getTimeUnit().equals(TimeUnit.DAYS)) { + return new Pair<>(HealthDataResolver.AggregateRequest.TimeGroupUnit.DAILY, + timeInterval.getValue()); + } else if (timeInterval.getTimeUnit().equals(TimeUnit.HOURS)) { + return new Pair<>(HealthDataResolver.AggregateRequest.TimeGroupUnit.HOURLY, + timeInterval.getValue()); + } + if (timeInterval.getTimeUnit().equals(TimeUnit.MINUTES)) { + return new Pair<>(HealthDataResolver.AggregateRequest.TimeGroupUnit.MINUTELY, + timeInterval.getValue()); + } + return new Pair<>(HealthDataResolver.AggregateRequest.TimeGroupUnit.MINUTELY, + timeInterval.getValue()); + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/status/IKProviderInfo.java b/android/src/main/java/nl/sense/rninputkit/inputkit/status/IKProviderInfo.java new file mode 100644 index 0000000..b515027 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/status/IKProviderInfo.java @@ -0,0 +1,23 @@ +package nl.sense.rninputkit.inputkit.status; + +import android.text.TextUtils; + +/** + * Created by panjiyudasetya on 10/12/17. + */ + +public class IKProviderInfo extends IKResultInfo { + + public IKProviderInfo(int resultCode, String message) { + super(resultCode); + this.message = TextUtils.isEmpty(message) + ? defaultMessage + : message; + } + + @Override + public String getMessage() { + return this.message; + } + +} diff --git a/android/src/main/java/nl/sense/rninputkit/inputkit/status/IKResultInfo.java b/android/src/main/java/nl/sense/rninputkit/inputkit/status/IKResultInfo.java new file mode 100644 index 0000000..6197101 --- /dev/null +++ b/android/src/main/java/nl/sense/rninputkit/inputkit/status/IKResultInfo.java @@ -0,0 +1,42 @@ +package nl.sense.rninputkit.inputkit.status; + +import androidx.annotation.NonNull; +import android.text.TextUtils; + +/** + * Created by panjiyudasetya on 10/12/17. + */ + +public class IKResultInfo { + protected final String defaultMessage = "UNKNOWN_RESULT_INFO"; + protected int resultCode = 0; + protected String message; + + public IKResultInfo(int resultCode) { + this.resultCode = resultCode; + this.message = defaultMessage; + } + + public IKResultInfo(int resultCode, @NonNull String message) { + this.resultCode = resultCode; + this.message = TextUtils.isEmpty(message) + ? defaultMessage + : message; + } + + public int getResultCode() { + return resultCode; + } + + public String getMessage() { + return message; + } + + @Override + public String toString() { + return "IKResultInfo{" + + "message='" + message + '\'' + + ", resultCode=" + resultCode + + '}'; + } +} diff --git a/android/src/main/java/nl/sense/rninputkit/modules/HealthBridge.java b/android/src/main/java/nl/sense/rninputkit/modules/HealthBridge.java index 8f55519..873067e 100644 --- a/android/src/main/java/nl/sense/rninputkit/modules/HealthBridge.java +++ b/android/src/main/java/nl/sense/rninputkit/modules/HealthBridge.java @@ -31,23 +31,23 @@ import java.util.List; import java.util.concurrent.TimeUnit; -import nl.sense_os.input_kit.HealthProvider; // TODO IMPORTS -import nl.sense_os.input_kit.HealthProvider.ProviderType; -import nl.sense_os.input_kit.InputKit; -import nl.sense_os.input_kit.constant.IKStatus; -import nl.sense_os.input_kit.constant.SampleType; -import nl.sense_os.input_kit.entity.BloodPressure; -import nl.sense_os.input_kit.entity.IKValue; -import nl.sense_os.input_kit.entity.SensorDataPoint; -import nl.sense_os.input_kit.entity.StepContent; -import nl.sense_os.input_kit.entity.Weight; -import nl.sense_os.input_kit.googlefit.GoogleFitHealthProvider; -import nl.sense_os.input_kit.helper.AppHelper; -import nl.sense_os.input_kit.status.IKProviderInfo; -import nl.sense_os.input_kit.status.IKResultInfo; +import nl.sense.rninputkit.inputkit.HealthProvider; // TODO IMPORTS +import nl.sense.rninputkit.inputkit.HealthProvider.ProviderType; +import nl.sense.rninputkit.inputkit.InputKit; +import nl.sense.rninputkit.inputkit.constant.IKStatus; +import nl.sense.rninputkit.inputkit.constant.SampleType; +import nl.sense.rninputkit.inputkit.entity.BloodPressure; +import nl.sense.rninputkit.inputkit.entity.IKValue; +import nl.sense.rninputkit.inputkit.entity.SensorDataPoint; +import nl.sense.rninputkit.inputkit.entity.StepContent; +import nl.sense.rninputkit.inputkit.entity.Weight; +import nl.sense.rninputkit.inputkit.googlefit.GoogleFitHealthProvider; +import nl.sense.rninputkit.inputkit.helper.AppHelper; +import nl.sense.rninputkit.inputkit.status.IKProviderInfo; +import nl.sense.rninputkit.inputkit.status.IKResultInfo; import static nl.sense.rninputkit.data.Constants.JS_SUPPORTED_EVENTS; -import static nl.sense_os.input_kit.constant.IKStatus.Code.IK_NOT_CONNECTED; +import static nl.sense.rninputkit.inputkit.constant.IKStatus.Code.IK_NOT_CONNECTED; /** * Created by panjiyudasetya on 5/30/17. @@ -101,7 +101,7 @@ public void onNewIntent(Intent intent) { /** * Start monitoring health sensors. - * @param typeString Sensor type should be one of these {@link nl.sense_os.input_kit.constant.SampleType.SampleName} sensor + * @param typeString Sensor type should be one of these {@link nl.sense.rninputkit.inputkit.constant.SampleType.SampleName} sensor * @param promise contains an information of subscribing health sensor. */ @ReactMethod @@ -123,7 +123,7 @@ public void startMonitoring(final String typeString, final Promise promise) { /** * Stop monitoring health sensors. - * @param typeString Sensor type should be one of these {@link nl.sense_os.input_kit.constant.SampleType.SampleName} sensor + * @param typeString Sensor type should be one of these {@link nl.sense.rninputkit.inputkit.constant.SampleType.SampleName} sensor * @param promise contains an information of unsubscribing health sensor. */ @ReactMethod diff --git a/android/src/main/java/nl/sense/rninputkit/modules/health/HealthPermissionPromise.java b/android/src/main/java/nl/sense/rninputkit/modules/health/HealthPermissionPromise.java index e5b2842..944d8d7 100644 --- a/android/src/main/java/nl/sense/rninputkit/modules/health/HealthPermissionPromise.java +++ b/android/src/main/java/nl/sense/rninputkit/modules/health/HealthPermissionPromise.java @@ -4,7 +4,7 @@ import com.facebook.react.bridge.Promise; -import nl.sense_os.input_kit.status.IKProviderInfo; +import nl.sense.rninputkit.inputkit.status.IKProviderInfo; /** * Created by panjiyudasetya on 11/20/17. diff --git a/android/src/main/java/nl/sense/rninputkit/modules/health/event/Event.java b/android/src/main/java/nl/sense/rninputkit/modules/health/event/Event.java index c478019..6f489cf 100644 --- a/android/src/main/java/nl/sense/rninputkit/modules/health/event/Event.java +++ b/android/src/main/java/nl/sense/rninputkit/modules/health/event/Event.java @@ -12,7 +12,7 @@ import java.util.List; -import nl.sense_os.input_kit.entity.IKValue; +import nl.sense.rninputkit.inputkit.entity.IKValue; /** * Created by panjiyudasetya on 7/21/17. diff --git a/android/src/main/java/nl/sense/rninputkit/modules/health/event/EventHandler.java b/android/src/main/java/nl/sense/rninputkit/modules/health/event/EventHandler.java index 97a309d..97c61fc 100644 --- a/android/src/main/java/nl/sense/rninputkit/modules/health/event/EventHandler.java +++ b/android/src/main/java/nl/sense/rninputkit/modules/health/event/EventHandler.java @@ -25,9 +25,9 @@ import java.util.Map; import java.util.Set; -import nl.sense_os.input_kit.constant.IKStatus; -import nl.sense_os.input_kit.entity.IKValue; -import nl.sense_os.input_kit.entity.SensorDataPoint; +import nl.sense.rninputkit.inputkit.constant.IKStatus; +import nl.sense.rninputkit.inputkit.entity.IKValue; +import nl.sense.rninputkit.inputkit.entity.SensorDataPoint; /** * Created by panjiyudasetya on 7/24/17. diff --git a/android/src/main/java/nl/sense/rninputkit/service/activity/detector/ActivityHandler.java b/android/src/main/java/nl/sense/rninputkit/service/activity/detector/ActivityHandler.java index 92cb924..0e5d4b2 100644 --- a/android/src/main/java/nl/sense/rninputkit/service/activity/detector/ActivityHandler.java +++ b/android/src/main/java/nl/sense/rninputkit/service/activity/detector/ActivityHandler.java @@ -13,15 +13,15 @@ import java.util.Calendar; import java.util.List; -import nl.sense_os.input_kit.InputKit; // TODO IMPORTS -import nl.sense_os.input_kit.entity.DateContent; -import nl.sense_os.input_kit.entity.IKValue; -import nl.sense_os.input_kit.entity.SensorDataPoint; -import nl.sense_os.input_kit.status.IKResultInfo; +import nl.sense.rninputkit.inputkit.InputKit; // TODO IMPORTS +import nl.sense.rninputkit.inputkit.entity.DateContent; +import nl.sense.rninputkit.inputkit.entity.IKValue; +import nl.sense.rninputkit.inputkit.entity.SensorDataPoint; +import nl.sense.rninputkit.inputkit.status.IKResultInfo; import static nl.sense.rninputkit.data.Constants.JS_SUPPORTED_EVENTS; -import static nl.sense_os.input_kit.constant.SampleType.DISTANCE_WALKING_RUNNING; -import static nl.sense_os.input_kit.constant.SampleType.STEP_COUNT; // TODO IMPORTS +import static nl.sense.rninputkit.inputkit.constant.SampleType.DISTANCE_WALKING_RUNNING; +import static nl.sense.rninputkit.inputkit.constant.SampleType.STEP_COUNT; // TODO IMPORTS public class ActivityHandler { public static final String ACTIVITY_TYPE = "activityType"; diff --git a/android/src/main/java/nl/sense/rninputkit/service/activity/detector/ActivityMonitoringService.java b/android/src/main/java/nl/sense/rninputkit/service/activity/detector/ActivityMonitoringService.java index f427c99..d36dca7 100644 --- a/android/src/main/java/nl/sense/rninputkit/service/activity/detector/ActivityMonitoringService.java +++ b/android/src/main/java/nl/sense/rninputkit/service/activity/detector/ActivityMonitoringService.java @@ -23,7 +23,7 @@ import java.util.Date; import java.util.concurrent.TimeUnit; -import nl.sense_os.input_kit.constant.SampleType.SampleName; // TODO IMPORTS +import nl.sense.rninputkit.inputkit.constant.SampleType.SampleName; // TODO IMPORTS import static android.os.Build.VERSION.SDK_INT; diff --git a/android/src/main/res/mipmap-hdpi/ic_launcher.png b/android/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..cde69bcccec65160d92116f20ffce4fce0b5245c GIT binary patch literal 3418 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 VPm_}$I;#Kx002ovPDHLkV1gV~5fuOc literal 0 HcmV?d00001 diff --git a/android/src/main/res/mipmap-mdpi/ic_launcher.png b/android/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..c133a0cbd379f5af6dbf1a899a0293ca5eccfad0 GIT binary patch literal 2206 zcmZ{mc|6mPAICqNwJrBe)OTwlM~;a|Uz&T!k&1?zWA4p4M`7F7`J;wP4IvS^nJ7{y zAtgDMD>=W8<&GtU-}>|S$M5}kyxz~p>-~Pb{(irc?QF~icx8A201&Xin%Hxx@kekd zw>yHjlemC*8(JFz05gs6x7#7EM|xoGtpVVs0szqB0bqwaqAdVG7&rLc6#(=y0YEA! z=jFw}xeKVfmAMI*+}bv7qH=LK2#X5^06wul0s+}M(f|O@&WMyG9frlGyLb z&Eix=47rL84J+tEWcy_XTyc*xw9uOQy`qmHCjAeJ?d=dUhm;P}^F=LH42AEMIh6X8 z*I7Q1jK%gVlL|8w?%##)xSIY`Y+9$SC8!X*_A*S0SWOKNUtza(FZHahoC2|6f=*oD zxJ8-RZk!+YpG+J}Uqnq$y%y>O^@e5M3SSw^29PMwt%8lX^9FT=O@VX$FCLBdlj#<{ zJWWH<#iU!^E7axvK+`u;$*sGq1SmGYc&{g03Md&$r@btQSUIjl&yJXA&=79FdJ+D< z4K^ORdM{M0b2{wRROvjz1@Rb>5dFb@gfkYiIOAKM(NR3*1JpeR_Hk3>WGvU&>}D^HXZ02JUnM z@1s_HhX#rG7;|FkSh2#agJ_2fREo)L`ws+6{?IeWV(>Dy8A(6)IjpSH-n_uO=810y z#4?ez9NnERv6k)N13sXmx)=sv=$$i_QK`hp%I2cyi*J=ihBWZLwpx9Z#|s;+XI!0s zLjYRVt!1KO;mnb7ZL~XoefWU02f{jcY`2wZ4QK+q7gc4iz%d0)5$tPUg~$jVI6vFO zK^wG7t=**T40km@TNUK+WTx<1mL|6Tn6+kB+E$Gpt8SauF9E-CR9Uui_EHn_nmBqS z>o#G}58nHFtICqJPx<_?UZ;z0_(0&UqMnTftMKW@%AxYpa!g0fxGe060^xkRtYguj ze&fPtC!?RgE}FsE0*^2lnE>42K#jp^nJDyzp{JV*jU?{+%KzW37-q|d3i&%eooE6C8Z2t2 z9bBL;^fzVhdLxCQh1+Ms5P)ilz9MYFKdqYN%*u^ch(Fq~QJASr5V_=szAKA4Xm5M} z(Kka%r!noMtz6ZUbjBrJ?Hy&c+mHB{OFQ}=41Irej{0N90`E*~_F1&7Du+zF{Dky) z+KN|-mmIT`Thcij!{3=ibyIn830G zN{kI3d`NgUEJ|2If}J!?@w~FV+v?~tlo8ps3Nl`3^kI)WfZ0|ms6U8HEvD9HIDWkz6`T_QSewYZyzkRh)!g~R>!jaR9;K|#82kfE5^;R!~}H4C?q{1AG?O$5kGp)G$f%VML%aPD?{ zG6)*KodSZRXbl8OD=ETxQLJz)KMI7xjArKUNh3@0f|T|75?Yy=pD7056ja0W)O;Td zCEJ=7q?d|$3rZb+8Cvt6mybV-#1B2}Jai^DOjM2<90tpql|M5tmheg){2NyZR}x3w zL6u}F+C-PIzZ56q0x$;mVJXM1V0;F}y9F29ob51f;;+)t&7l30gloMMHPTuod530FC}j^4#qOJV%5!&e!H9#!N&XQvs5{R zD_FOomd-uk@?_JiWP%&nQ_myBlM6so1Ffa1aaL7B`!ZTXPg_S%TUS*>M^8iJRj1*~ e{{%>Z1YfTk|3C04d;8A^0$7;Zm{b|L#{L(;l>}-4 literal 0 HcmV?d00001 diff --git a/android/src/main/res/mipmap-mdpi/ic_notif.png b/android/src/main/res/mipmap-mdpi/ic_notif.png new file mode 100755 index 0000000000000000000000000000000000000000..6a1819d3bbac5bde9d4615690575f863e2b203e5 GIT binary patch literal 385 zcmV-{0e=38P)X0(Ea`z3<|sZ0@ATm7FdH%XvKPYV94XS z5{bAY3#`NRICuzXd*&kLVmySb?n71G@}b9#A2P&xqC)ozi!5Btdo4V4b=$29%#q=COH$o faTBbZZLD%Eon=|IMTLT)00000NkvXXu0mjfW`Cy8 literal 0 HcmV?d00001 diff --git a/android/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..bfa42f0e7b91d006d22352c9ff2f134e504e3c1d GIT binary patch literal 4842 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 literal 0 HcmV?d00001 diff --git a/android/src/main/res/mipmap-xhdpi/ic_notif.png b/android/src/main/res/mipmap-xhdpi/ic_notif.png new file mode 100755 index 0000000000000000000000000000000000000000..46a213bb7ef3cd7292956e3a58436d3506d0cc7a GIT binary patch literal 837 zcmV-L1G@Z)P)b777^{PzqG)41d`=K$Q6RSm%h^h{FriguUtj3(DgmL|gR_FdpX%z-5SL z!AogC!pDO!4;o@oJ_`moV+KTL5u!Rkey##;Lc>vbW%2+Abb=@vpXwdpjai7rYqZJa z0rqGJ*Q^^Vgrt)G9VDEA=-%dlbxqCnjr{2=zv3rf^1sy2l<8vB)X&b1ezR@ z#vgGATHqO`!Y|SBVF<-GoJKe_h{Dhg9;&}{j}eFI@KpS9H^6I%G`vEkbZ(G7hkwHR z`H{FNgyJDK;D6Zbxyw2fYf%tZ#p5h!j}2IlmT*)&_7iB3twsm@12Te0{b&D+ P00000NkvXXu0mjfT!VgI literal 0 HcmV?d00001 diff --git a/android/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..324e72cdd7480cb983fa1bcc7ce686e51ef87fe7 GIT binary patch literal 7718 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 literal 0 HcmV?d00001 diff --git a/android/src/main/res/mipmap-xxhdpi/ic_notif.png b/android/src/main/res/mipmap-xxhdpi/ic_notif.png new file mode 100755 index 0000000000000000000000000000000000000000..7bd13647bc3093198fc51f3c03a1a29d0182964c GIT binary patch literal 1080 zcmV-81jqY{P));&>#!GNQ{0b z5;wp%E$A;KOB@~ZQ4p^miy|?u6WlC!uOl#4%u$6OjY?wU?vPoE8dcIAc_`>B_3Q7I z0@@LSAalK~f{umG{UH`~HsWI3 zR7+>+UDVla4t6yvNWAwQWX8e?@ihN0VkpD;7O(SOfy_^~3fcopAoGFcg7o-P;9I^B z-VGIyInPEx4t|ErV(e$Xpm4MPHbhM^SOV9mAlbe;fR@0I zU_o8g4}FQPg7!f%WWI=CL7ze9H!uFL5@!p>!S(*>cX>F(4Ww`J6{Ez1DzYQDTvqJP`_{tds-^zT2w)1D7Fl{ zApNLRW@45FjfYGpcoxbz6iXmejzjg>unRgGtJDg3Jd1*+sV}g#rQ^<6#354;e!dHG z(H=6Buw4!Xjka()zW@uNGh08R3B4fofIp^I(S;cnq!08b8T^T-4+130MS7di3sQ4^ z2$`k$D1(B0Op>bzTctSL&BiC#W&LaW;XSNCqRf#ej?G!n{)j^+NL7&9ww*ZRG=Z;g zl+}Fg@i2;zC~+*)|H701g7hg{C5sD12dCp{G{kG_{U;KXA48(V@CkM|J=(W}%$J7q zxfqQ^>4R+y=6)0DvKSq4s?GmRtihU0!&VmN+#UTP(*fHU&U-l$r6s<=h1edB?HjoR zaT0d0IH!kWa5|2GZ(+_u5ZCR9)lvv$lt5?SLNm#q3eFX)vk>*izl!7+e}z>P;M2qh y + rninputkit + Actively syncing steps are allowed + NiceDay is actively syncing steps. + diff --git a/android/src/main/res/values/styles.xml b/android/src/main/res/values/styles.xml new file mode 100644 index 0000000..319eb0c --- /dev/null +++ b/android/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/example/android/.settings/org.eclipse.buildship.core.prefs b/example/android/.settings/org.eclipse.buildship.core.prefs new file mode 100644 index 0000000..e889521 --- /dev/null +++ b/example/android/.settings/org.eclipse.buildship.core.prefs @@ -0,0 +1,2 @@ +connection.project.dir= +eclipse.preferences.version=1 From 3597821e10945c682ed95c103071617024915cf6 Mon Sep 17 00:00:00 2001 From: xaviraol Date: Fri, 20 Dec 2019 15:44:18 +0100 Subject: [PATCH 10/13] The app runs in Android. IK doesn't work yet: INPUT_KIT_NOT_CONNECTED error when trying to use it --- example/android/app/src/main/AndroidManifest.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index e12167c..e430398 100755 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -8,7 +8,6 @@ android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" - android:allowBackup="false" android:theme="@style/AppTheme"> Date: Fri, 27 Dec 2019 13:57:24 +0700 Subject: [PATCH 11/13] Added debug keystore --- example/android/keystores/debug.jks | Bin 0 -> 2088 bytes .../android/keystores/debug.keystore.properties | 4 ++++ 2 files changed, 4 insertions(+) create mode 100644 example/android/keystores/debug.jks create mode 100644 example/android/keystores/debug.keystore.properties diff --git a/example/android/keystores/debug.jks b/example/android/keystores/debug.jks new file mode 100644 index 0000000000000000000000000000000000000000..173ac3bbd623c37d0a524b98b01edf5ac0c13811 GIT binary patch literal 2088 zcmaKsX*ARe7{=#6Gq%y#hA>&PWN&Q45MwDzj4d>XEHn0HCVL1Y*+NOSn#va0N^0yy zp{!wSQMiO`g(*X+u5-`*a6jD-&xhwd=RNOxp68t3@6F$vAP@+ADBvG)dju0Mh4>K& zM9*-a0AlpvJj8@+O%DWO0ZnZiL^ko@9)TY`)B#$B$^l3M7-M4Y!}3T$(>do&ADJw1F_^|N?YRYc_Ov=c4ga$ zrCDWqK=}BL(25Qh9kx(IR9-MJ&m$U}a$QbifKBF>oTZk9Loq=tO0wM~?CnSFdC@D( z4;bpck%p30a52U0vJ3rs_7{=mX|KCTGci}jPMW0*`B~mwr-BPfrbZr-1pRidhK3q) zsk~&{Fs*5;d{~F1*y%t%>WqDmQzC>Tb`~t}+)#j7*qN1eH5u8vYP3FG;IZ~b_1@Ns zn`jaBVDm`apY?tj{vxm_CMw62cqCdN6Js;Jy+MB0b%#zB_6!(@ zQYd`hv<}eb9G5AqEQxy;aO5APzURds;iHsIY98)$M;zE3Z)xXjUuvGN9bq4l5|Dt+ zYb=E8?2PeO&GDH&JzsvOblQcT8N~$_IQ07sVGKu@jTP>SjAKrVm?4{M222zFHDP~2D z0sRTR?B_a?Rb`IQYWy%XbP1pMNo?9Nu#Ku7+5iEZ^u2o-9BpbB@(%`rdXNE23>)$T zv`a1X=cD`Iy@vbb!;Hv53EQUWe!Rh^SKzojIhV(QZkz!vpGP@F*x0W8n43xR993|5 zffwXHF_ZdNnUVIO@g!R@M$4VD_ENcS5bip+)xQ!w{V|4nyPHe-rtT{F{@jCaxC=am zuK?lSELIG~n7WlDt&_@_boEqQeZ%5nC|_$ES+A_?rn@pob>D8iQhA}pM_jl(#KkIP zZ9&aq5%f#A+|{qWNy&&Mb|NTX>b@v?Y*@Ej>+#K!62h@@bVg%Da^1nTBE#@z@wC~N z18JbCcDgNGyo49#+oG2@C{oQ6PK(=BCDEJgYX*fvblz5Z$;b#OKEKONdCKu+tc3IW zr#HWA@y3%gJayp z>;2wD_$nD07H4h_mBVnIH(NQhVt>v0)aoD%wfc*-K#^cJBwYbbG8{s8%o|}dhwGZQDV+hIo zG&^>_>iLDMjJ-*-Y!C~xTU$eo=`-xwiY-!syK~ByI+T* zVGfx&iK2k%C<@T}00IWUV5oiepbiSo%As)0>>3je0Guo!6jD_Pb(9t20EHm9&k=(| zi4tZ+kH9cr2_r|8m>^sgqpGH+g+i;VqfpL*a1D(^i~868?=A&6{@+uFAcjx?ZqVU3 zFfauGKq<{0GLQ|&-7V^^x`N$#Hdt|#`lX{IRfr=ywOJ#*?K@t4a{kGNSCwQ*VCqHW z>+cfQ;ZKt|0>0_9BF^D-80Vq045*l74AMDxJ5)^#?41!-ik4(?u#uvf^k?j$Ba^;I z2)-eb^7WY2uT_A?s>CLU>#a4!;84GybyQq?(LL}Zv%=*2Q2{+0cVZSNu`;)*bqdt? z!i~`rZ5xI5VJS`l_`HjwW{(*sDJ0Cvx_N25QiOT!q(tf0lP{!C&-vCoNsZi8N=Zbn zl*w*e&zZ%RE92ARKSAVW_MbAiT!aTCkXqj}U4+Ih#BUi0E;J?=Mn=`Xqz+9pH93UkusK8)x(-h=ebO}wKy?^#B^T;=SJtJV#4$DPFWhKZzO2+_OCKR z-Eou;38&Gj{86^@rf;T;Fvrt>2?H50OZtzjzMH(`6?3Qj#rOtZg$l&y*76=is zhO5~lhVb!3bO36sGGB;0Hzr%C$-=fKgjWFVrdfL-ejz)zjn~S9BiEoQV}GZ?m|q}W zyKtj3zkjMybeBvM(Jw%lJNKR$f{m4Eij3Y}{x}d8^~R`*=UWZVlxgVTw@&9MzFbY; PbMmqDz3G-(e6iziYf!i` literal 0 HcmV?d00001 diff --git a/example/android/keystores/debug.keystore.properties b/example/android/keystores/debug.keystore.properties new file mode 100644 index 0000000..805ff40 --- /dev/null +++ b/example/android/keystores/debug.keystore.properties @@ -0,0 +1,4 @@ +keyAlias=androiddebugkey +keyPassword=android +storeFile=debug.jks +storePassword=android From 0abbc672e0f8638d6d03f5aa77ceb166bfa0f169 Mon Sep 17 00:00:00 2001 From: "Panji Y. Wiwaha" Date: Fri, 27 Dec 2019 13:57:38 +0700 Subject: [PATCH 12/13] Updated example android build gradle --- example/android/app/build.gradle | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index ed2df06..661d2ba 100755 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -117,7 +117,14 @@ def jscFlavor = 'org.webkit:android-jsc:+' * on project.ext.react, JavaScript will not be compiled to Hermes Bytecode * and the benefits of using Hermes will therefore be sharply reduced. */ -def enableHermes = project.ext.react.get("enableHermes", false); +def enableHermes = project.ext.react.get("enableHermes", false) + +/** + * Load debug keystore properties from example root project. + */ +def keystorePropertiesFile = rootProject.file("keystores/debug.keystore.properties") +def keyProps = new Properties() +keyProps.load(new FileInputStream(keystorePropertiesFile)) android { compileSdkVersion rootProject.ext.compileSdkVersion @@ -144,10 +151,11 @@ android { } signingConfigs { debug { - storeFile file('debug.keystore') - storePassword 'android' - keyAlias 'androiddebugkey' - keyPassword 'android' + + storeFile file('../keystores/' + keyProps['storeFile']) + storePassword keyProps['storePassword'] + keyAlias keyProps['keyAlias'] + keyPassword keyProps['keyPassword'] } } buildTypes { From 7f341a304fb275b52ee68a32839b79ec8f5ecc48 Mon Sep 17 00:00:00 2001 From: "Panji Y. Wiwaha" Date: Fri, 27 Dec 2019 16:28:17 +0700 Subject: [PATCH 13/13] Commented out `apply plugin: 'maven'` --- android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/build.gradle b/android/build.gradle index bdc4a76..b2ee783 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -20,7 +20,7 @@ def safeExtGet(prop, fallback) { } apply plugin: 'com.android.library' -// apply plugin: 'maven' +apply plugin: 'maven' buildscript { // The Android Gradle plugin is only required when opening the android folder stand-alone.