From f176f294f773ed39830984a5bfd9fcda24b8d22e Mon Sep 17 00:00:00 2001 From: Bryan Roscoe Date: Wed, 10 Jun 2026 11:49:28 -0500 Subject: [PATCH 1/7] =?UTF-8?q?Remote:=20scrcpy=20control=20channel=20?= =?UTF-8?q?=E2=80=94=20key=20presses=20drop=20from=20~690ms=20to=20~ms?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Remote tab's transport is now a persistent scrcpy-server control socket instead of one `adb shell input ...` (and one JVM cold-start on the TV) per key press. Verified on hardware end-to-end. Backend (adb/remote_input.rs): - RemoteInputSession per serial: push the bundled scrcpy-server v3.1 jar (Apache-2.0, pinned SHA-256), adb forward a fresh local port to localabstract:scrcpy_, spawn the server resident via app_process (control-only mode), connect and complete the dummy-byte handshake. Connect retry covers the handshake read — adb forward accepts TCP before the device-side socket listens, so only a 0x00 read proves the server is up. - INJECT_KEYCODE / INJECT_TEXT encoders, byte-exact tested. UTF-8 text now works (the input-text fallback remains ASCII-only). - Driver grows spawn() for resident children, with stdin pinned to /dev/null — app_process aborts at startup if the adb client's stdin is fully closed (found the hard way). - Session registry on AppState; slow start runs outside the lock. Teardown: socket close (server self-exits), narrow pkill matching our jar name only, child kill, forward removal. disconnect_device drops the session. - Live integration test (ignored in CI): full roundtrip against a real device incl. SLEEP/WAKEUP injection verified via dumpsys and a no-leftover-process check after close. Commands: send_key/send_text try the channel first and fall back transparently to `input` if it can't start; a failed channel send drops the session so the next press retries fresh. Results carry which transport served them. Remote tab: hold-to-repeat D-pad (pointer events; channel only — the shell queue would lag the finger), near-immediate typing flush on the channel (250ms batching kept for shell), live "instant / compatible" transport cue. Found by the live test, fixed here: adb forward calls without -s fail with "more than one device" the moment a second device is connected. --- v2/README.md | 10 + v2/src-tauri/resources/scrcpy-server-v3.1 | Bin 0 -> 90640 bytes v2/src-tauri/src/adb/driver.rs | 35 ++ v2/src-tauri/src/adb/mod.rs | 2 + v2/src-tauri/src/adb/remote_input.rs | 586 ++++++++++++++++++++++ v2/src-tauri/src/commands/devices.rs | 3 + v2/src-tauri/src/commands/input.rs | 95 +++- v2/src-tauri/src/commands/state.rs | 105 +++- v2/src-tauri/tauri.conf.json | 3 + v2/src/lib/components/RemoteTab.svelte | 82 ++- v2/src/lib/types.ts | 3 + 11 files changed, 904 insertions(+), 20 deletions(-) create mode 100644 v2/src-tauri/resources/scrcpy-server-v3.1 create mode 100644 v2/src-tauri/src/adb/remote_input.rs diff --git a/v2/README.md b/v2/README.md index 0c18e1e..fdfebf7 100644 --- a/v2/README.md +++ b/v2/README.md @@ -198,3 +198,13 @@ Default in the plan is **Svelte**. Override before running `create-tauri-app` if - [PLAN.md](PLAN.md) — phased porting roadmap with milestones - [`../docs/FEATURES.md`](../docs/FEATURES.md) — behavior spec (the source of truth) - v1: `Shield-Optimizer.ps1` at repo root + +## Third-party components + +The Remote tab's low-latency input channel bundles the +[scrcpy](https://github.com/Genymobile/scrcpy) server +(`src-tauri/resources/scrcpy-server-v3.1`, © Genymobile, licensed under the +[Apache License 2.0](https://github.com/Genymobile/scrcpy/blob/master/LICENSE)). +It is pushed to the device's temp storage (`/data/local/tmp`) and runs with +shell privileges only while a Remote session is open; the process exits when +the session closes. diff --git a/v2/src-tauri/resources/scrcpy-server-v3.1 b/v2/src-tauri/resources/scrcpy-server-v3.1 new file mode 100644 index 0000000000000000000000000000000000000000..e80ea2a650658b53d9dd9c7820e6ee61aff17b82 GIT binary patch literal 90640 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 z z1Sn6&Dfkpv_HjjE3oLoAABy1QsjeTgUhZv_UO*^7=jISRK2G=??__^BTDqY|hVkyj zdq1kj)L$Z#_tKS}b=^i!(C=O{cF8<~&>5$x9TF3mTdpvM2(^y?q_d76%UI`n!j{rQ zs`qOP96NYggPZ{)j3NyK&wi{pMbBhGcuL#?#{?Gx*G)VOU6?V5RAHCHal}L zK1lb&A25%Tm{%AGHE<$(Gno!{d3A6k*ki`hULn}R>B|bSM=^Dowev}uF_f=cYPiHX zJ4EZlt``SB*0{r_9FKB^VpK z+g(JliZE6S{4%=^L}Rk!OJ`rn;fBROZIV99)6DxXp40!RbjS%|G+gXq< zr{B+E5yN?ZP``&|Bd9$AbEn_eC(Z-C-=}TN=6jLZTQeKV;(M|cDeW_Uefl+v?=ywJ z6e?XI%O3Wwe3>kTSU+XlCsNk^BISNSq`VJ`ly4);WNH&g1;!_P=DD-)6_+9YPk>9z zoqZoYhv?akU>I^(c8sob8}ApFMeOPLMb2$}fSw!a*`CjFI_OH;D|PqkdjXWnB~ufU zo_!oPckYAYvRE<7;h5T(J9i_$74jSD>{YrVpftHP2>SE$Snj)cEMnzSv1Dr0S{lN> zI_i0$*0?Co}+1K^L)`+NZUBRNmyu_>E|zQ%X$ zzkid$$$wOc;rk5YlacE?kMHHdTATwm%tWb#YzRXl2d7Vd=-6XCn`e*lY%hC^r$1D> zaXV58i$sffC~J+eSzBX-vi20uedyRzY{K$C()JXKdfCX-?Na!r0?Q1G-E1SXifvyW z5-RM6KsGWeY%_ApE3{q0re5|iGk5j2hwNkI?)y$9uXHmoP1JBtaL>JOKE0EV(e_{y zuLd>oYS2j5D@P9t!w!oqWelyMik91BqC>)PmAhAHKf63yLLqaVkU~2Y^@U-dr&l~2 zukVQz^_FLdT5Azki=5}USl$ZDSgh3uPiJrpwHWW!T+6848I0o-?b52H@%2Q#oxuq@ z&Lm5nqy8NB;#Ee6C&cB1VZCtjQ}3~C{sg4R5zPPN?><{2;DTqXp}FE^i}nG~8lz(u z1A?!-M~2pMBcRUo+202Jk%QQvKP;T{tJv;&mpb==_5phIK1$mY^5pC%^Ynm@57?z_ zRc(z&rvDh1$1HY_gzEEcD0VmF-NozfCqr~_jisJDk$1OCqy1`-0;qS}iWX@tovSl= zok8C7$j*}d^>tWdfW5rG(jNRFS%Z3NJ%x@=&qJ}P4itMy-kSchUA=WQ4~w*XMB_zj z=cV}laLBg{Z4X|+I|s_=kTup|ku=B6zUQT3uJ9^@4|BF z?5IvSPHQ`lwdz>MdK~6+H1C4zi2Ry0y1&u(xU>^w3$JR^%xMqKva*_D_^xi!Gw?^3cyGV-V(={Tl$3(&fFO605vlI4$P ztz!<^Z_RisuX{W6ZR;+jqi+uu@d!tPX<)b#X=n@$f%zzdkEQb;Lp1g|y8MRbO(Ma7 zfb?k_)M`TX%Y0wM zOS1NyFXr(NFZSXeg^kQ^jFUMi8}q) zvH76B8TXYsBeR9ZHCxxAs;;rWLCkEy)~mWM`mgHBdp#pd*<)yK&2VbAPo6kHpZvX7 zpDg|_`^1qaHfwu-XMCJ8XJ*TM@&ILi*sIJ7{_Am2c{k+s>0^ERv@zSKPaU97KkC(| z7yd8$)L9R5d{dB@<{|O{tjP;Tx!$wq!06QX_uo^{J8eu1k;sQc=34(Lt<%1fzEb>J zd(dSln=lvYNDv2?v7OaowiqNlZ?ex0y3^@c74j9#>j_$yk3DYD>CH|&ORbdSooI3e zsCdBf6g6_-zx69+)7c4o*w0T`Y?b7{T7Yx*tHK%`7_Y~nMcBg~P~mGkK272QuC<3bnr5b4Z%R2hjP-CN5Ep(W`tE_SFy=J z&t~=EA%=4keuD7^<5#kGWDW{ml~jKmL)gM+A0X$;ym*cIEtJ=hUF)H+8_JKgUABAt z3E#m?kLeEc31#h;>b<3SodnCk?`?rX@)CoiMifnP*fd+h1%({*R>Wq{8H%r?)M1#S zpIQiRvMI~3&15B0P(ZzUE61^EZ2{ETXlNoXXfuX=WxMPT^8|V5~b8as3=ez z4A#W=5B~ZnUsv%c6_Bihv)+Ro)1f?HCDa5LL5nBAC9Gr!8VLiv1Q$obT5M^!z=9g@ zFeqO)(TChrqA}yiHwpq&+wD*s=;EBWWB8{OUP$3v;SE1>`R|3j zCfA2fSvvyA8*Pnu66Rg&n}9WD4>;BXpvoAf$_UP&cmZlP`Ty)yW)bE6MYh-S%L^CU z9KMx%XS;4K*W%ST+-`9w6zvOR&T3Q;{^>k1HPTv}8&duj5S~s3((PEVkmwk`lcm`Y z3C_g8Yo)wNo{u-ThR>(JQo3d({l6yl{(7~ilkDygp`_x<5 zSJEfm5zc$?T$&j=-h3Z&&Hh0!?u8$KF2i^K-NgknVf@|0;LQTx%nBd2#jkML3*5_h z<4jVx>~8J_ANULGLn|Iy8i#!EESh80zPdyHclr!{b5tY}-d;bSO0K(_>Re`f&#j?#bf8 z6TO}A1G6t4baXV|eWm2E8sxg|V72hk-Vv0Z!eW9qgroY5Mb|G(x_*hz4xj4Ut@O*C z!SjV+K7i6#mGpXTgNGi7`56Hq6C zl#2zCryZTg=bbasTLi*H%OMDxyC>wOw8zi*A*L%J-cG z<1z1P;K+i5;qc&S=fo3&dv%Zf#EhAK-tSk+J{geXs=LyRC6z~vOEFdmWdrSh0^gz* z;glu3QdLO%9yKS}8F?9{N9x6SWH~Nq-)?8@6=Q~_)}PdVakY+Mfn)PDl<#iV8}b^w zcnzt0Ir{`rHf9zay<6!za_ty?z;;9ozxZ!!y)#?u7me!dnC0q7{a2#vc!;iJB(IJ` z5kiN+!ENxAF9-q2-O8Rojc=`IXp=L(H?SRA@6oOGsIhflmp{5bJ+^^crQgzH`~BT& z-(;>&hV9=cIF6ASWErpfp!nbQ$Zn@cVlNrhW}wfQ(Kr!F7o#hIkL}!-HzYIQJ8iT*1v+zhK;;##CXlr*}-~c_VnAm*=D9srMcG<`+=TX`y}bMg-K^cn3Rg`Go_dPX>Rl)9?BZ z>C@u(tP@l^5t6ffoc={lKg4Ju|0_ns`1g6dC~~%=Ki0T2EnX67`3Iy0Bz@)Tc$ua# zVR=7$8k3N;UqbrwDSK74gLJZ=3TqGkQ~OUdwH$cuZe+trnRqG>1}*)hIWM@_pC zp=x@v_z$82XKzPjG|v6b$T`CC<)+GCx7(qin?aw375oEI(FG#X%U1K&Y&%}YcD#b^ z7z6odt=zR+!FICp6)w*nLnQ{YB|7EZyW2S@=apk8_dtr?W}l z=Ms`uC}<^R-n92cC$wR_ZyCTF}Q+Uw#lr#e@{ z&;yhJY?t7aQ3g3Xf_L=%PuBfO*kjrwHSG1RJu(GmL*tPz7=U(Le74G`u3yUYR`Ex_^xoyCNI)(M&Cc%?dbeI1KO!~ zc(8z_725ZDxSg-_Q}K80R{Cfs{?cshr4`RdXjO*zujhj$;`Gx`pIU{Ln1oolM7;R> z%chRPx47t?C;ff5TkW?=2DdAp4>*gP><Y zxHFRB(9XzSgZ#G;rKvF9z_0HZ#r(CJeXSDT_b!K=K_NIwokD5o`ym{jB9wR87NGsL z)PsUgu>O?pykoRlgru??p`I!KTG^OUOp9sjXV`Ai360l#?}+-<&}r&Ah8hjxkd}s? z59&2C91XG?xC3a{#uSDCofD23#dop4-RG;FHMQrb|F)cBVy(>1$Y>5DyWApE=Wgm$ z7S)72Ipq8%kNsNd>By>#ECkwvZy7?b53~rSUr?u@DgA=bwHVHQfVH-1y;-uaulC@Z ztcRu$!67+a)YPxl{uw>uKpGQZTj?4%?_c57Q(D99Vm%a&t-HcQx|W9+M=9B|3&QS! z`l&PcG~sr)dLx`gnk?Ew;)V+My%6?1h$;Undtijyc2D$ppnO2B$Ap<~Aw%x+;_=mk zVWue<_UdpJwz!lY<}}6x*%P4=JGe~`O2%W4ryam*O*Q0p^sbIy;3T};>+H`tU#l}N z9Xp`mjFB_&8fj+@ljm^neM!zA?Pc08Z0i2cOQt$=d>)o{6cF8vn>5?ACx$zp|%hvQ240I84s|3*x^; zqB`ORdTx)Lim@7J|52>O7$=)Vm(L)L59akZ?58vNxp5ATS!AN2b_Y#Xd-P`io!b*Y z$yNxpyNBA7-xh+PASk%;Fs&PSoI`_pzIM*L{F9cUl*iz=_1mu@=Ub{TU*Omd{N!sT z%T~bK*xnD3Uz}I|K8_Nt&@{2v-OkAAVg{Y`R{oagtZNiAf>9Sex7Ss}j9_k0u7I1q zR=W0N>~DhbQUyE)$uWP!wj5&+wfzmW$kznzixDzC<~@;&y@T-acg(5P5Yy=_xj*68 zorYo3+VW>4nVKCjFb$M!3QZhk#E%j&BJ~*cPaXCa$!UdZtViA|56jZ$yT5jL+4rz6 zh?RnEv`-J=|2=)SuaZ3;_*&^LI>Br1?^EX!ma&iQ<5R~*PQY1PdH8E*ef6EZ_0@NJ zt*`#->zqHM3q_0gIJ>_3QFeXx9|!PfG(NkQ`XTv>(`%_09>hP>r^2!Ap|zf0YkEm_sl1v$^0~LuNe7>QG3Nc<8!;;)Ls#?HUnSx_A#HsiJ$dazkEaQfol&I znz=LW4EU4o+d+~`(5HnbBRlmRAfKy)CCTiA@oE;`N<9c@lyb-z)<`sU^I3 zbh`#2cfK=k-;~gO@5WpoA~!4!?f{}N-X5p&u9 z#60?$GHXXE;Y+8@A{pE(1`E=L4;CZfKy7wbyVvc^_s3iT)j>zTZ-rfXK3%{5TJg(j zob8=W!LNFaFYIx22>s1`n;8qk*Yo^T9uga;eI>M)M5F@MVj>)&bc6Cxp*$QkPXCI1 zT#$ZnKyNLd_HEsJjWrB8pST^t0j9|-RLbI|Ums05^&w%A_DC4xCF{Ns3xdU@mzhWU zdA5mBIY{#RkFvKh(KxKnc$H=KlEu7Efny~~xg7PL9WgqO#t{3LHH0iQC_kl<9iU~z z(OFH#*-TF-?nh4_F><({`gZUhrOWHx9|N?mHI}tvY(q@DzsCrb-`H^$kD1KcN=S(A z$<3X;|6%S}TK6yt)6@~{5B)|oRemK-F?Wd?^H8WT$uIo(1o0sze%-@-h>79IheY-; zAEIoL9>-7zN-D&=h>?nj7RT-~gC_Zo-z}n{3UeEVhs>cydg#|e`#Lm|_}m`ZY0k*= z(_Obm?fp3gp_pJK8QIO}kUJ$L%Xed%yAckNrrzMHNVLO#eV@VUR7eT+(b@*ZU$=VsOK18+$ z?;;KZp9e_dZ4Ks0`%B_mRd@iOujl6$I7d`V$D2>HT_}m8Rk6tL(JGzVhtC`Nx!Dfm z^CtZ~Q0C6wJ#UZleVU9>xgo`i#fqeQ4{|%3Vzx|EO8gh8dG^UO2_)iV%W(s z1CaCCx@?a+vpd-_SknIgDtw|E7aPiY(1q{)WV!=e!r`_K4{&(nf>N10r$~0jMwk%; z>(PNN#JK4Wq@4?blJE?9U5-{}VSK@0%;hNN^V@?vSsOvEk1!9*Etly9MNZ3P(*oP+ z1(d`;0V<7n=xTflNV}xQ@Wb0kI%+J(Hwtmo)z-Xo3T5&zrUPtT1bb}dh$eLu3GEYJ`GMY$=AXeoKyOo zVySb*x(||Xkse|@(hJID)SKiMO)^xBGO1cprd!e;Io*^_Za|)yxk?bgz zcsDzP=NK$!b%bQT4cO23=soTT`|ZJjCOr+7iZeYL6lXd(&kpT@Jr2K9X*%{S^#0Hu zb>2fV7Lp2A6OSE|IUXD9i7=NQQ-bhZCX>~JWRAZcEGP1rGlwCri<<3`V`vBPxI+-~ z4#7LZL9CdezBBfsGzFzN1m6`DD2v&htWQ`F>qM+d?JEGud-9*fdaVPJ9gj2ST@seG zQdlgW7g2*wQ=PYTAba3?Jp^*TL{0(a!Ex0gc-@9Lp8{f5z_=N1D3P!iU~_rY5F?^e z^mo?K!?MSdTt?%vmsugtS+qhoej?y%=aS8cwP+`RmUB&mS4&W_H%dJa-vO>IvqcO<51lnVVm{-!d z8Qw5#XUn!Lp~71%$XB`-MtIel!euPq-6Ve^a&Yt39!HKTk-g=Z68Z1WY9AsGL`#Oq z94%30Zrh{QUYs?lalvAK9vdvgwq0Yi@S6WT=Gu9`gJIL2hajCO&>4B%bZnU%`^Ud) zxNQ=LvA1qVPWg0C_8e|`FrQ-%-T3DowdT7&-H0N#ABk=FsmAuh^XSH5d30lFmTsi& z$~;O@kw+=YrK9^`yVP&=SsSsaY#cKjXZ;6sb_dOP9J`=&WJYS68{_^epC_F&Tsl-C zJ$nR>(*-zA1q?~#8+;1TUCL1edi_wX^xs6it0+Gh|Wa9|N zr-XMqg98;tYOG+4MA!&Z%^oUkqXtTJt_t_Qjl+@iFPlsbWzEHOjDuyENJs0`zLoqm z$+;B@|Bh*#T`pGw;a~FlrSM#O_V*mDiLJ$+-cOTJB?Fpotkis?>J{Z}Y@p^HvBwO^ zS%76Z$&E}0@70{_``RB{W24F;>k&pcI-lJ6?3LfBHKAlA>fqJLFl-y`S?CP@iJ+!@ z7|MpDB!wtkn=T(E^JR}w-#EB-tj4t#!^PNd)EZ1DY%|H){C(o~L}%T5X0W<5vfZr1 zFMl-Eim-Ybp>*e*Z%Eqa*V zPdS(6dir-*boIbKFpTL7Zx6bGDA}dT_Hc|`ilE2u zY3vw|?;|BqA>qN^$NYCNRz+BY_xNcERCtO7VOzqnYF`wse+>ks`yWaa6>;qx{QV*i z-?n|@;M+s{;M+s9^xXNAOt*`&aMX8yj)R*QeB-S1jm9=jG4uBXlHW*$n@7t9!O14% z*$U_5#sh^NOz#krX*ot7$X`!PCd8>=)(FHliehY#<_YK!p$TzYlP!l^}xhDi|9)A&#VhC*?Tg7G^oVUZR5_6>+E>`b~ zLmYq{BH1rbuX)W7QlU?u-^Eqtq)rL9|=o+^0?u@N9W}6i*>a`jkZ>1GF9_L0tKWYEF zm}WSyP&?lzINu{@TQj*QUAzM%h6*`|%wcQ}S(#xmztTUXs4tHpUkhz!Av4jVGVP<{O3~ zqF)tB2V?!=8)biF&##0wF`RW18BEJw&#)&5|ItGA68uodPBob^gNOl|5(B*YjXo20 zKRxkOY~Nooyns2xDahqbkm=$&#Iz3ylR3TV_`rp5*fB6XrvorV@D zl(gGUvbOSzePLs=sZNB9{v$u`2}azbXZwlXj##%9G}4i2*xMIdkf(1e-mB%~2(^dK0h}sx+WaZf(fbEjJB4%} z`uNxGjpcNtKb-te1qx`K+KkWW2z)85)ZGbo3yWaFn`55 zQP@O}8{^c;j@WVLAWwemiJpJ#<9vhDD5g2l+_8^agZZAS+i;XgXO1oal2P07TI0SD z$U_-faCTu|+qPYHC@e)jDrzbkVV6Y!{+f0u_#o{6HDjF@7-P~|>+Qi|7M&EYLc{*q+3%y&cm2jT z+CHK|KBQcYW&>mHp^$59O@TYFy)|B|Pn&VxEuU!l_q7MlqP~BbV$NWg8sQn?DFkt= zf{B263ZG&}P(pdh1LS4t{-f1eo!=`eKbm#4*ZzALM!u?v4j#`atyrH2Nav)I@4Ydw zO3Mn@a(Eq(e>u|i{T}btwBjq2jc|b15@+c(MOuR=-t#CJuD_4t@HsF+sQvg}cuHP) zvwRLo@v}&lpB7IEe0>66AIH~6#l!Tv5nms`*Zagh^!jsry%S$Ih+D+PH_J8RDG9er ze7Z_LMYo6W=J3Ajb~c@F&sUP=R zOa1=d+*;~IqD34jowd}N(pgJALhiqoN_(=^TIvk4M%Qbp7xh|8&7H+_lrA}%T8SayJ2Sz?Yk|736riX$-25h(K= zuh(a1NXN$_)$NCSwa;ZX#n)zn~*!tAt4>T5q(**x3~UkmJDzU z2ricIRXQr=lgfmN>`!vZ5uK5R2Aw#U96t+T>k@u$5B`w3w&eH(`vIb`^tc4~NipU} zH1pEE%FnU~=}3+3hJ~Wl43S3y!-MdUl(VFBipH{vq&Z9G{3mR?YNpO{#^>N*)(O0cJkZJxjr^y%xpYl^i){;B|ym9tpCuTGR!aHjn;Wbs$W0r1yx@;}*vE4$t!a1_zcm2+wheZf zByG-H+9>Txdz@WryJ}F%)@Myi`>4u=mxu`vq|79eg7 z39`phntI0`YkE<+IL`l0Ufhfnlo(W zQ(E!MpX)xi`CPhkw%9M!eZC97&DMRs2xVQrNZvYE>Qh1teK+m*>4_xIVwVppPrpRi z?gqCD!k)>NUDn`ndF^O}&*iZvW2nzg@HNfXwgv;QiyKUruh{1S3M=+`p}|l00%)*Y zKI)lrm)|$Ur>re~$k!G53VmU}>iNf62L^By(}H8NKGeM5Y9AwYvvKwg!uwo4p@!E{ z%PT`m#6TINb4{c$TI8+LE~clj{F~Jt8)`)a&Ra}LsYA>sQGeLqDqFZPjC3Whnj*s->myl$>@0*+Fegm=%D-h67t{wsvVD;J zk9|O8IDK%2C)Wo_NvT7;&PI8T_*So9&!2~R9*cc(ZyrqF$IqnQO}41ANKecDcClSH za?XZ15U|V=elknJ9@bzQmOW0rV;zp<_awcy0FKM4^kkn)*J7Oa4FcndW1-me@^i63 zH~|A`%|~2oN&SO$qxyv)XA@ujt-@#81}NAZY(z{xSE_vqlE=69TZPX$f(uQHu=t$0 z(%$Y^=-ZXF;~d+7BV+f&@o*7r292n@&=m#+jq|hK)cZ3$4&Xfyl=o5JDm_F;u$!bh zNKV7NowIeY)^=%wOIn zB}HbT>5M*N1G7lGXYuLrDui5K7iMAL$DEffIwJ)z4acf+Oj_sK^KWtu>O5`8?FC!@ zt&K?l(Il&+%HI!v|N0&ykv3#MH#ncxX4Qy5vCgt`f^53>cp05;PfM;=X=xWyWq(ZT!L%N?InaY?J;*V5 z+?K-IZlLtT-F{bkHKxA?(^urDwoj;Bn3taGis?;EZ(w@E4fqo}53YODbqGOt5Ahh# zn$VHlZ?q<~K^}oW+5_R|zH`AZukWYdEu%8;f!Z>7VjP?c#(Vbx>^cup=USc7WMi)>4!15&6t}fKQHD7xIWl( zUhFxB4Hv?Oi^8}rQI{I2riXAY@`zPvelO1R+kz7rUcBr}GK^H2L$XeHiW+n1gUfzpziP45@D@%S-%f;8!1h^$Rz~Fr(X& zcb~=b?R}D@sAD12OL`jNQ~V0)A&8GSgltcUU)FT|bjbM$8?DE|DUz%0)tw#n6HbB|9QI{UlPZBr_S{2 z3|>we6XXStML2#Kzl7zFOdDbSQ7AV$>Q!DIFwR+7 zO4A7~JZ~y1me73h3=}cCyLs*iX_4HAccwmdlBmy_vgyu@jD9~q7n%_cwZ*yM#d+Yx znd8Md;YB&ui+Om3MtSh(=D8<@HBe$|spwxe^@a?|@cF67 z?4Mevw0fmnpOMr@#5<2CI)MFmLLM(jbo_i!`<;kRob{cuhj-{it4zzYWZGzxY4hwn z8}rNmruSaQT4i!f8%h>uV|y2~C1D|$3>#hF>GRTpS1?{XFl>Y+!b0pZDEAYTcn=|a zCx!;A7Adp)cK2rYZwVR7 zY7w19uBVWF=}E)Y0EQcXo(9t>Xs`@lL!sfvcQ3ci;2qv%p=Gs4qU22v28vPmzRF#> zjHHeM(~{q;}XUl|;afbO%4 zSdX$qICgHHoqc^RDqEiy-*bfOf!f!5mDN6*1*lsje4*6rc%oc-AgdaVon>bu@) z7Ra1m$SvPF=i=SK_H0Gk1IF{;slAT_p$ZVDZ0r{Uc^ngIzu6v%SiFBB@?*9~WiCN# z`i)$nCtUXN(-;FWnX#7gE6a}Jee!$5G4V#UJRiHNQY%uYD8&&d?|8319*RYk(IB}D$-*c(et4KlvN_>6+Xi< z$xuNJGf8+wep3*Q!Q^h)qbFhW)IyXhhM-+Vys`kbRly z+{o{ao3#4nY}waCXIaaz@e>Xk!+SP*h0<5wH1>YoLmm@6VYA!aC(Ii*|6=tC zD+rrcIPnC9AwKq1pqy6WR;PN8Fx-}S{d~9Eut<+mSOH4Q|8mwp*Lk{c&IjlGN9?hE z3U7tsdMhk?tf7&mBFJNZBd?EjM((8jF$njCG7O(^Z_^2oA-!QTsByE>8B2y&H|BXJ zI6lrUF?0LwuEc&nvd_LdIuW}wvR$1s8QEbi;gb&DVXylR|3H3p-X*CQ>vacCkph<; z$%RZ{UlrsTl#BI0e5RX2iz^d?IpBk!94dK2- zTupr{AmnlI^niV8JuwU$^YqlGA7JgJLhQ!beA7YJUHa+q=+FIVu!U9x^S+JqKFLGt zUk`E#d3cuS+mTBMe@n|%;riVDmIe}NVD zPeuu#fTfv73ZgUfOQ@Vg`4_kb`Y?ZAf!X52{Cx#Hqr3%}?_1e?-8{1cNEeC>v%#(A z_FYDFaPn1^AFX4OcBetQk;B3Kq?@%!hMO#%&XbjMbLqLCjf$eLaJ1H$pNUXWVIW-e z4MXW?*>%b?P-|r|#B+0itl9(64ax@1K-j%e$PIFizIW3-=R;!3K83HsFbm;oo8kt= zL_V38pf$!jv)-}Px^h2+>c3J8V4x(c;&S%FX){is)!)rQA)+55|Nl3#OB?_>o$heR zo&R?9KE>N4WBK^1E|336jyX>{7-8 zIR{%Gvn%;JR;?H<1bc?!mAdj)&CZpSR|s-toW9RlhdN)cL!H-W9g0iPW!;ipi)+tY zhdN(U>QI(8|8QS#+I$|Lx32ij^Y$rUcVT!iJLi)ppO@88AqV5Ni;25JS^}ohN|`DS z;!=|}N#r}&owp+6M#)YPtafDAc!p_wIvjHQIiBC)@FziWFdyzlc18X_`EdX5(jLes zyhFn;-lzC{qT!n~4PPuBF5!Pqy9a7`CEV)L{k~GuC*p}8QhdJV6^MGPa;Su9Mv*Ku z!`4%r=aJbw^Vc_=Jg>^j^Wpy@&&U2vo(@g8exI`0=!DJYzoQ3x5MMud5PIOqBBkKi zc?q~>pOU-6uqg|7AuMS$s9k5uU)T-oyVsc;8?tj_k)9ii_gkawg#Fj4zvs@)jYX1D z=j5AT?sMdu3s}Ax2>65y_=fiO=dGOqytYs2$C7myVZV&Z z`koj^TJyS#b7Qw(Uro;&v5RChm>)YV67C$upzoA_eqX-Rr!OyN8#-m%_MXOfEwB{p z6veR`c2L>lU{c2rOq0R%QBQAJr#v0F7_9Lf*_b-G_S1dpthyFhLNc_?>)9@8*t|ME zpK0D4K8c>JaFt*41rT-*hrWAT2D7{pmamwVB_#LG0hD5y+ZkaZKBoGDV2lrYD9S}~^b({=2KU-h#Q+8l2a0&Jga7+3MlPv-H>n@J9G9|fIT}f}tG)NCsd-2ql+--@7tX_pR&E}Ci}UaWo`;T(i%twDUMU${ ziERnt{ffNVG|tMKO&Eh>IW{Z;#$#H=A3R|tPrg)66vL7BUyxuJce@~udp4>yg92RSrjDaP+r=eM(h3@ z&0o-r2GsY1m*vB$gh@?)*)=1#Ly6ve$aX~g|%D)WhY^2jL5ia4&tudTZb7xi4i@oFRrP0RxU}-(ugJ!K>)*zA>VA1%-QUUbguGX#LhFFYmd!*e#G^h< z7WJxo(E;l2@I7lWZ*?Zk)rePyXV){y&Vt8(Ke}F+rzgHzM*S2kEHb?`&{s>!FaI2> zTlG9^forth`X~G8t*?>vHOJT8g=rK|cCFT1|KvaDt*@07H=nP&U*q)_xDN9ub6}eJ z%sB_(GdJdOc-Kiv70U9_k78bq4*aeEg$`WdeF4A5Ymn#OGg(@b!FnBto_I75?fp0O z#Hru2m~V-mw_!0Rk0vM{|3LZsHN7fiJgRWa3z$zNo6k~9(Ezqq?4^sWEa%yto#~jr z%hBx3*P6WnI{z}r$s@1zs^@3!|7ktFdS1Mlg-xm<`eA1tz7C6r_Ung% z=0fP*5C47phv@Pmkdslr#(Nx;Eb|X-Pj`c?kprLxdpB&|V$j|(3-M0!$aM#mRg%-! z{ns+j{&LM&J+HRtTqA?`L2V(rUbP}&!p z&u5~)h#re_jTeZ|8Hi=h<2HX{B|BFme|0R`Xd^yLo~vrXbYgtvJj9yH?u~wiwuNJU z%e4Gv`rOsBX~7y99hIN1vb1+99UIae82g+x#4C2>+Dd#J!9KQ8hUHGBBQtdD&NVWL zcXbw*vb!KXO+UT%I}Cqu$nj-W*e>FGlCiajx5IePl|#Dc$qv?+>FTvI>J7`gvveSh zX(%0)<;&tw#g}GzV$6RGkJa^<|45|5G#WXL*$j83(U%BaYn>%D&z>$QU@ICUc{9BaXvOGCHDH zieb4Fxe>#d3vQx%=Z#ocmU>ma)i`0C&YN(YZp@Ao;qMAi=QGhbRp4&|y#oWMAmY=AsU z0`3sean#?=Dql8^nmtB?N1B?)jT|T*!9T^cFs5aO-77r!tY*8#x=xRp-(6W<=W%*| z&(53Qpm31;mdN|Lir52DjpEPcx%P$Z86(*S)IOy;zIN!>IO{n_0lH zAPW2i$t75KMV)vA3Ix{O8!q@(@BayCoJ(?vkK16e?G3GGkgu1Sa)Ue+Z_ZwWORzmN z7#2QZxutQ=O_Hz_`KYcQ6M`BS*2$&nMCZ*Xk6%Y;7BgQFjV9-;%j$5MQn(DM<(qK+ zH|!Z|vCEWU*GdoG4J~UOaL>YIJ;w38F}~)2V?5FOKOJL39DIzOvTi(JS(Cm0J-tw6 zS^q^@>kn8~YZi7p{>!pl2P=yJX)gXwz~8YbUB)2|f%?><9vCWMR!^c9#<12tuwEA5 zE*Ce+u33m&FxrWjpqUbLqLUG*Fj*qr)&s+I!fGdB|J!9(Q~hdr?kt2ej2yybH)+sYpJtq#Y>?YYO-bd)%~e8$FQoPb`B(B;o@%6|$Tl;=0q zw`Q;g33>jkdb;pKkA9&2n{gCDC{tL=H&XgXcrg@0GP zl}@DBT#$bCg7l_!D^jhgsnB8Z7zWxB^tUp#F>&1B74>(Hs)|>o%@Y2Vf9at&Fd;U}PeNR6YPl;BPAa zit#rI0`dn7;R;H;3i+}?rn&2(t~-Au7mIKJF9QRxoq{h|lW1L2J6d*J2j9o2=i~2} zl;$nEY^BT9ba|aFyErrI>g9AKsGJU$oA#(2g~JiSnAG;mY;RBd>%x9 z{JdO_SS}A=E@v-aCXc2l_z5Ajf8a8C`^A`S_AEK*_RBWN6Ggv({QmNla@2BpBt@Dk zN&*?#Opj|QzU;YpxeQTW8EMbT$WxZf%@9h80P3hPje>Km1)kv ztrT=R{@Q6^=w&Hg9y19#(r^(2DUAhPTJT33z#gTSMfm$24;WpZrOR*V@&sKFplVa` zs!<5G-KjB$kHyxG#<#h6hZs+y@d&0X>T&kg!JwWCXkGnPIEG;bKd-8fcZqCV5Q zR)m6y*7Ql+x_esN+R~}!sjK(h&|JH(q4uQOef4#+bz$~74%cC>J$Jxe!lf{1K>q2X zk@7Ava$9OgPrSSJ*q->*)V7TcJ@F}`q$n=>hk6hd#9v*tE&foVL6nq;;lX%H3@#D_ z{lC3*V{K-A>t$jT1@$WuBmB>aar$|*|JjzF_*(uif4CeolPBLU$37TGK_W19bt>ML zLDWMV$R|Lcz3z@Fa4rN=tEsR>guJ^_@%mIlCO(DUe@vs@wK~3`2h;d4%}5Mb4{mi9 z(jXx24RJD%2kI#C6hQ0K0bv(8{|g|b(M=o&sXu0yNySl~3b4(?ILs)~+8y7zatcZO zfiGdL8ifiT&2Fj=G7*@nwYFynoCSd!u+vk>KL=LFQ(NFCgfpQDlQh+K%Eb#)X?Q|1 zxF}rw8sAc|h3}uk?-e@!oDnCN2;Y-jH$=Sh8L&eh~X71uN939m1p5!J~#uHIj>qDf@o9i+?~hor!U3r^n|tCr*sV zVIaj_l{jgBYJGfF?WuAh;@6Z6&dM}zgd?erfp>{jNWc)vvTb_gh9-BC_(s6A%UHV(Q5z?L@5STpq$w?!43JYn2+h6r4rbRY9$y zw3#YGbm{z;qg3?c9IV2j7=8}7w~HPxPB4PK(=xf)BXiyC3k0Edk z(JATsu6~7F@F2X&1PLBBxmB6Oip5)QNWdnGW=ed+)3vF!ai)8THSiaNr1t0F77GHG zx567#?=2ZP3R9n<=KT+6wRU$cTAgUj(Av6%$2xEWDuXAl)#-`y%oi@Whnj4Fap zC`RHecw5yah_{JrVLXB5P{b=ea0pJU9tP1KIvnOhVm1?CGsyO*M6&Re)rm|SPT-P& z?7ls*I+2orGf~)J_$*epa;ZR+a)v|kSB1YZgoLTWm2??R7h*w)Jaz5t{rCCSygv%q zx^ut3{o>?(*}0Yet1qALAB!Xtnz}HSszmHH;WUNUM)vf;D5_R9UM_w3EX3i9uOoZD zLHJ=fvhl}K+t#dHkyyQY6BHn+CB9~TPXZUgi|}!E8&W}GhpOOMMTj!pYgQy~fGCalqRFd2sLFK3 zpGJNe-?zu>;U6pzFr*=i0OQp}8@Xot&MArdbbK@#L#R& zdi{DvoJ`#q0gW8hlbX`Qls*wp^M5G8gzQ7rNd{kW#V&wa4q3HIc3%g_I5kO#enB{% zN_&`EyIIlbGq=D38nDd8u!>WEz9REcV)Lf65oNDyZG(xlcc_OZ^9JOUdSD520XU>@ zQmxkZ5EHvUswaEtnr=l=#u zHDyB>UAVc5rYm@LP9s%Nxj$AI434Jv+7sXzhQ04YTKB-z)6;PlqU8r-P}PcbSG=|hAu^Fb2DTQ{ zaH2m@*KVn8UR2fGy%iQ3+(#!>J=|GUwf@+)W1nky{_d)Hd=vIU5B_RbZ(Ng>r+j zuo1mr+ZSWphhQr8CNkaXcB!JypM3&PO3o?)H{-)AqBw}7UxJ=!C`%7#Y>cTrYAlcFD5NI69Is*53+l&Jh{P zk*gAyCsL`)6EYwsDDb=bYFV^ge*es+3`DD%)3QGS<=JOmaZ;kYGn0SkmNw$4JOfl3_5h*CghQ{F(g03dUOvva2Nkv5; z4_CK&^fi(D`U>|nqGeH1%))a5p5vhTGPrzSB9qFrHm4F|2o7;@GY&w2S;W=#ndYvZ z`YlLVdPKqe)EdV%8V%U%|}5wlSq&-IZxODS$$@jG*?}TOhaaCD~1Qt2yZgb zq~^fROr|G8vGITAAyFPg+LcLXME_EliN)d6T0U{&L^zIy7-up(!rX3#I18|JXL%iN zkU4*>71Z&_wDu%2&Bz?xeG>AF!?~+_GUwfK67=WKA{d0RI%0}~85zzdmZs@+=#MY6 zt^p4Z+(Sx6e2Ve7`ZyW zb?tAFQF(6deO=;YP_s7+c1&ufGW=b1Up)BDQGts!XCg&T34Y z;$;#nrYqIihVklCU9m`re{TWYAeZgsh;|tXBxT@(`7OZ z-KnlAT?opE_UaQ;+Rjd-S9HaH0DmXW3ERJn28)^V`mU~(iTWvt^~kUhkI;7M`#ne~ zdc>sQ(F<6m#N_(Pa$RsB7{PtWPG>26Y&ABm_R_Y0jiu6VIemTbp|} z;>etWBSR{Nd#K=4d=zp*Z&Laf9l5PG5wGoGip5`J^{ri9i((m^Vv8{6ttU6!*drG` znCYpOXD>u_y8BI*i>gMS^fdg2014I;?6(~emx~u>+K>|8 zO7F6m=A9Zi!jRiEpp*^ zERbiO?tzo2O|zNdlGr@^UgD{^_5TU%w739mJg>qq&Tzsaq#lV0l@rwa|B*veJkpB}{&KT+r z_#IRXznaEw*fp7kTV3`*dB(|Y{ z0R}6@S_6$+Y4D0C|AO;svEG>} zSch7y%NVF~a1;*yT8$u5nRFT^QOl7A!VC=AL@H5!OJL{c{Y znu$?X!fG52W%PD)R-d~)j;tlDb7pA*?pLEN1_u={IW&~6U(bKrp^2tVD;&wBt;|lW zT}^*0s0o=al)4_{X07>D7A{AizvNJ+dPon9ul<8xKnZ_84N>JbjNYd3_fF)qpQiO3q%gzYI6p4^F5J%KQs({4j#1^&gK3fI1BO>N|w|*b1-+!y$SLpj9P__h%;4^+t>Tr*a|Ac-I>-+IK{3v}t zS>OMzzwgrb?fO1Re>cH7vr*j*eUAwh785Z(H}f=++e07$vU}hizpbX;fu^4QJmb`N zmEN)SE!E+ra4g5KfLUS?f;Oc)6b7-+a(X@-!P?1Vg5C>RqkUjpqw~5}-*3?O+jRT~ z@E^TDqVswL(?6oWQO(qS3fIvu&J<1?28$MG(Rm%J+kdDyl(8b+kIMFmicftrUWZN5 z`A!iv{7tn=eLF*(LGT4<>Nw|$bGT0~5stlx3is%~^5}H^L_aQjGNx&;LW) zyTD0xR0-pCZ$Ib3Jb4U|M~6%jNCGoMNQg`lzhDz0Qj7Y!=OV#J6Ml_)0cCs9;1sHmvlIj5@bt=o?o zvY-DR|1aetD3Z=-Z>QKmzpd>&!_0o&9q>@~@<@VPPJ@z7J`#E%=|u^0xyw zFi|HDZZZ=bk2>`wdTY%5$m-K}(SF=7`FV}_ zzD0aF{Wl1@TP!+T2>yBj9}{I8Q|kXF5qGZ$&k^-p01}4Awg@L+M3Zb5`0o~VUJPo= z-wW7_vQ0IaCQeF??)c-s9cc5Bhst7)jZrAw-J0Y%kHT}v6O#XV_)VI$t&QQ0n&iJ0 z@x4=g_X}COpMBKI>nV$;InGPsTdQ&0HQE|Y%H>f38`b`eN!XhNY@;Q=v8>g~I4d6O z4FYd2GvK?b z{6@hWO`<;YwfO|UM$lOV=@tn&#AW1f>ATpHaV}-v<#JHUZd)e075&0(%?a(AN5HPP z_z$_2)8l`)YxcZpr@NiIfRnkEOS)5pq)Y7YO_haj7USj3%x>!m zsk4Yax>}SKbvoWAqnbzH4nn)}@9jb_8?$6HY5+9;Mf$tN7-$i6llZM>dUb%gs{?@m zt4EjqTbRzIjYX`+0xS-&b`e;e932aoS;I&KhU3|MA!h^@E?ztwA2)LG#bXe;m?9Ug zU^6Mj!uas8VVZ%2kxl|w5EVK2C;$ugM&S{3l8L)nlK>@>W;PX{B$yUp9!k!1_*WfR z7)&Q}nPeisY9XUkGHwP&GPwY23M?EhWM;q^ z&g2q-%!W4^!}%plTfkfkAa`&cVETd;>{b2x2`FkZlMZyWw^v_pY|a~L3a*O8`a;oI z2)@bj_^1)Xs9ZXp3ao&lyt$Cf8P(aGF`Oi9?AvT$B$bKhJF5%nBsBDRz>JS)Q${W% z5(2qE-q_q(JrU1MVnHX2;iU+P4QHT`CPZLwjmMMeg^L1jp&m|-1QyyDOEw4YxjWFl zq&;xsjR6PvuDb&rOWG}##Q&TqljT!Rij*=x{BJZB>&eAOyD}-@mb(!f8F^_p7UWhX z@uE&j@{a){Wg3;!LoJsrgq$UM^*^$|YK~Tq=Ye@P6!He)f|J8cvKeTRq9{A_STa3D zL&_c)kLP(J@Z^%4iy{i9k+Wh-+aBuCpD}^4pqO_%N`8<_is_GO#Fy{#RBH!<%`eEiqm zboWg#FtacsAb|$rV&Nj@TDWK(c*5}S0j7Zxw+O1mu3iK~(=cf)LWPFM0ywmX1Nq4; zNH*58Xa&SmEDj07%nul*kq2@*QoA|8TsPm-$u!XMZUq^0Yk*|Tj9UXxIm1XtLHgXy z+_wfM?`AqkQQu;semARHEM&;tEVbn3*Dt&^&=U!+iY>W$(d%!8oL~$te4u^t>)v?F zqQGX$3y2Vz$;YX~9MA-g>HMS<2x8O7Mf36DjTW4_mMj4w(Z;IU62?SZVJr#l)6TRG z=IXe!ow=4S2j{jW9ZY{6KtbbN0X19^Sitm7@Z2@Hg1Nz2!Tfg(r%)u_!@PI#uYM0S zGc<+ko=#|6aMm&H^~@CvcCq?k*I;;Ec<}z%;Oc=;u$#>)0>ru^!Dy5<1^dIXr5&+g zZ}&i7xI1=xtX;sCDzFX#yUl{#Vc{*aV9PDo?Fvk$y~BdtY2htbU>&T1V0T)u*Re)| zyv~Ajv~$pH?KY@Gfb2SSEal{HTdIH}0?n>M$87>+*PvsW0NEAjxSiDn`}-r|uHay} zuQwL%W}e_scNkXA!F3&MW^fqh1z>}<@ifTu+>$rNC*nY4WF(n3*3xL@pgA1Wmo?I> zB0b4eUgFLa6Y4rHyG#yk4w{qc;r?6(XY!0dy^(-pxlB4!Faw-aAXiAIjT}u@fsuF; zhify9Y)e@%a=A>7)e0aDHP)hrGFjqo7-YbDYk5pUjQte>5*7he2sR*FOR6Rb4(Egl zlQswgm?edd08M;*F*zt^(wrt)2kdIPIo8D13&^>&AYz09+60EPj8hQg8V*x>#BAy%_;C>mQA9vB=7Mp(nj`v*ar z=?@LWqQSNO5sXfzVfqxgtSlr`3AMIb7n|J`j6`Bw>9buy24} z-9@VeYw&YHvm!8(19*Tni)a|-!x<1qqyy9m;7EKJ2b6^b1Kd){*>wUqp2YEnc*Vkj z-8L;siTqf~=7~fZ5R!mk7HCN{Qb?sHp~ZO|srb!yZDL)6sOE^G7f4bwFF8`B%jYH3l&(+r1QWSJOwB$h^~xMc9kEO-GYU=3 zTAi_}cmd`EAf3l`7s|bOHXFc3O>^~$_YagHo(>q}SJnKM)@#@ec}d z9zO8@+6)3hRMZwx`!=T_1rqflCXzHEdn1QULjY3OgaGPF8IW}$8wj`c(YlSqBV;e( z`7tO^BCvKSIvD8f8^mQFRwtJ$WU=#W32`XP$<eAnT>>ZfOCz6%`ZboP>lj<6SZZJ|#;9B5RsE-i(PM|8quNF~2I-eQN zq>6HG5Yez$6th{A0+AD6y$$2Zpf-gRAEII(0eTbH(bunz^NXv8x#zv+^7*) z!1eN2W@IFmPmUXzLVf|ewhY=>W>YMmDGZMp<^pz08H9X%GL}ilHjRPsiDgrT(NQA- z%bx{UF$)>`1)`lr<)bBDq*B~cf{`#r;)PVc2$R+~m65Sqi;!DNk-0oziz~&z`VN`4 zn}-BPPm3NccG!0;VKX zF~yt&`5Sy|if?c^3W^CffO3*woJ?C?bEh-jlHgEHpqUvDEJzf_v*x5JqZR~!m7y*w z&tpolZzQ+~SspldJj)h1(XwNcW^x$t?76~Zy-u9}=T<7JG?YGXj4K^`s~t}(2tMBo z@HItzB#$c#QrO>INaoq~PHfs7GbGPSCTvlWaEA;Ba4t}|h22pSqfCmpD|MZi(#QE$=UJxDKIUho2Nk9luRcwo2Djc?Nn^SeHB2uQAGlopQ|8`1Lg1>0{PZ*7!@V5 zB2G*rMNGhlA#M(<9a>0J&zVMU!dd@9dYT5kVM<&J?ba#LiW{{&(UNApt_!6oN?_+y zv0*ZhhI+05fjZwpDHG}G%4I-NTS}ktf|;iw8BeCoz%tIGIPGTRD8&+(5T^G`+QeNU zv}x0QXKQP8G7V~FJ~`Z#0>zW*-QCR79a=fGiZynJdV)id!B}@_UAQZRTNW)66pr-_ zz^Wq_?i~ybtb_GZb$2M*H4yGcEx$!1z;#KiCme!BpIEN=y2H`_Nbr7EFFZh0ED^f- zG{|Om8|Fq(D!TXrz*c~|N%aa0OKlAY7h)4s_Ji1khazzJI9GQ9pwOe50we9}q8QUi zs|)z~BHHRIv3E#mYj`9tP*u0k1s`B_+#Ada{j!ON9ST^B0~V!R%f;NAOXlqu+U~&4 zr%@EJMv-3#OPCj>5BEaTH{}Ck@d<-(Nk)hI`}+n4VP!qgHyG*~40Xp=4)yee2BLwH zRDASSWhpcaS~Q)MnE70imR;8gx+}_R6oFuWxCF(iMG#7}Q>Y8|TL8P`c~CD!AWZWk zYCS4MsM7*sEfHoF0ak-LkTR^?9E$uL0mKd6wHcUhtZlH;h!p`0DB5Bou#nwQ4lk3I zHRE^-+0v=eRCUHm(^@W#bGI7UZl1upmxPS0vrzWC3)vJ*|DcM|0wsxjT89Tiy?waH zfExx@SdKm!jt&j<1iM1a6M}I(!2F@!uDSnnA#73E_d@;`OmN z-ojixkqGnj3_zO!?I@ewLz@#gjSs_8&B=_i|K<50D!cZH&OwH*gjXHG6OL_4#ar;NUp&O`BN3eOW7dY}MZJa5iX zo*s~}?aYTCJXjzP9xiy%^SXA{+>^=CR*V$7paHNZzSlNDf}d}Lv1){krZOAiDegCo zVJsi_lM2adS4EU%vU~N?JC>1qJDbS|buP3SQdxsm3o}<3&u_vj*{pEcMX^8J4O|)? zyr0bti;>dByo1V9mtjd1T8bPy#1 zP!GBkiHwCV#`N~|v8%(qYk(DD_QV}TKB(6Ab%$cX=>5H2?50Z5_Xfi-`uYZ7Qtidj zIX&V)C<@axYZQn|Ci7KLG2!x&g4k>kS+W-y?u`xxdqJM~!hO6U7lixL{ki0LJU5BE zEJE%%8+%rGK+LuGa;-@AK@=Ku{d#swcr=Y#Vpbe?!b*oL`lCpL6dRm2=(Ye5OAqrM z6(gVwnE43K;pVskr6e$*TkJ@6{~uyjh3$2PiCbPp`-POR0V6j76_mrlEZIhEhxSK8 zj%lfiK!b5}BQ$6lbTz;T)%!+}IXtbAyqo-1QE8k2W(cs8EQ zp>{b>Mx`_9#k}W0D@kONK=8)0aa6kmtRv`#1JY=ccv2Tb;VWT$@qtATh{XrQF12Ej z7@nm=ffvft&F7+}^9#{Q9M1z671|KJc?gz~`*c}VP zv=(B`R=6uX&^5HSClb0(L0UBsTo+QpgW*Vbh}B!+p|z3V5R4rJ;F!UU^ufLu&A6cf zpl(dUTre3BS60%6REmJOKPVVq2*3!_BVF*Xo4F#Wtb+re8W@Dp7|iB4ed`erT_Ay6 zD1ST=l3hOffX9m-lt>Z!0DMiPCrro$jBwz3{12$CVfB$UYr-VUdU}Y0JF+H%QHUM^ z7$ik;2M$ijB=o!y$^{6~H%wrPgr;?27Jvbe^$6mGBrrk+rwpR-Z#}y@66YhSEuPJ` z$%9h9-D-BSxz6Y;SBD_3ligxP4fBbjEhw&RKr}zLYNf*^BiG5UpB}lBU0Z^Lat#&P z)-X(Goorr3gtCdTchlhOg#}$FyRHlZeL)w@q$aSGbIYS_&UdmvrKo#z&@z~KK?z3j z4BE+-m*Pw%!&}9=t&^>IjS2CNNh{>Ld9sr&`){P}WNoi4RVQmHO+cSc)>;yi9anh? z*CORu1hD!+Z3VF|gnKz8SXH$-^#4va$3hx~&KXM%n{5Liq|{clBE+(4kY9Cb?MwII z&SX$-RNB?MZo0IctldHxgC+DPn9|z#hRV`5ad<_0^_Je<$zDHQ3UOK!g=CPRRRpC% z-^uQNjRc|~?3LtguPwRCOO+A;JPs2=4jK;Hs1>B_6*Npys!rB5Z3+-i`CL4iHWGOC zGK!Zgiwk@EbVT^xKyjnobY;mp*}~}%Ew)`W6($uLR~5uW9n+!?@FlD*To+$)66%fV z3nS2{Y(d=j8aLbeNZTtKfOkxrO!QGvx;v*&mxuM*xFgxxc(!bvgvQwB+_sn!bzL%- zFT_(KMJKzV6qPo^+fAZ@l zrA4;mcv&jPqx3Ir4<4VJ%;qx{B4uaT{hc=QZIR4yJQbxq^e8MjjJ&FF*a#vjP$?>y zNWh#2%re`;zzYlgT>uJphV5=)Wr%1yVGUDWltfrB)IcDM#mMwze_1Qo^E) z`s1KEr&La}fT+H-()mUvH!Z?l&WhOxszk}49yOlH8_K|IQDdXD?bL+}YBfi`8`T!d z<&0`pQ+|%0com_r}L)T`mk_FwFPrI6ztiG5qBvJLJ^V7sG2ds`e9jJ7I>G! z0|XpEmAA7j^sZt8xa+Kr(TSw7sm&Tqw>aVDw$g0nf5{afs|x?ZE5ujDa79d2y>K8o z_x9}4wqFstTHc$lIK`C~CML(l4OfD$&fpxaXw%HDTyas05!Gt7UIDH;Ov+-ZqJSe< zr|#;^MNzn;Ar_yLHgWjVII8hIR+vctvS(K$OQ*Zpn7C*Hu6g z*Wg-P<+~s2_%{+C-;jv6V{cEHw3GEzKu}H+%FqvHlWRd?Nr5(=f+bNqGXC^*Ix5#! zL91M7WtXTt!T_rD<)gSBG*wgmH&b??-Z*7S`&J;1ps7$Rx6i$q+{SngPr^(T3gwaL zWT;KOd)~=zyJB2rrd)UhiMXy?sp<|)&c#coM|E6zM|Np{jcGbr_w*^mDdki>`Zu7m z^lw7tHk620Or@i;&?Emxe@I`6S%&$Qj5g;L;w#1%8h>RLD332IP)T$ar7F3T zTzQBTrA$rTLw^G*Qw;joKxJv=;aY}D2ku;5F}8Bgrt;`1O@}J%)VPitOPy@(YeDB% zpQh@YN)yl&{W?{9r&^km4=N2nO|C;NV7p4g zO(4>Xt;!QMVHI6htGp&9>~LRYLC3cNL24KisE}2Xpc1XOkV9&2^uP|3@hoq7W>tDFg@pP;&o(m}>!XXU;)yo+HXhtImqUdl`7IK@HpYG*l`W|sT}h^vJ(5AiS%s{P=g07xj1B3{L<_@XJxL>#5Pf}JNh}v1N+xPCaE&YWOe<8d zw}29aUu#oPniP~iGPLQev^ZFIGNu4p*9{jcaRbH(zC%w=;09O8Zs$xDvye`gaxXyf z!HTne1h7K6`GV9rJ~+%Op#z<6C854+!}yJ{)}i!pyf8XuH;{GBtf>UqIf_WU^4cs? zz(Z;4HB`CgxLQHY86$1?8u5+LfFLK+{56Fm8r!vv-EV!EnX5WeI^}TBWeBknszkO~qi-DudXvmw0Nn@+j}M6D1;FyZa}t{NBwVL-6f?@F_yR^f@89$!1baE z1>{?j&XIZTRHzx+u%8k^Xtb7;u7qb=LZBE!7P_N+^nmdudbL-s@NMOjM2E+WL?I>9 zD21$087qM-ONGirjq!MPER!=Twi{kAA4(_lmeegqfZ;Ko9BvEJ$I@UHUjecrFJBY- zT~awE^pQfEhB*j0BSH28Ecr+AIhgu4 z1)4YB1_ydEf%6hf$%yqVc3Tc z_7UFSM@C1YYzBtN25mIFKExU^h`v})FcKjnlnrY`(P(g0C?=zrYi+m-$lz;6Al}#8 z9R;K?kU`ssXwLu%d+^zFge_Yex6L})b_JXkemY*aw0(0&`8NJ3PneB=IYsKIM-wv^DJQZ6vyyHr+ z%DhajxA)tGslAd7IAKPkH$&#Ss14}-FB+l2r@kc-S^gpp;@0$L6oW0}e5rYBU^tVV zT%3)AngS)tZ^DPX{EU&zdcm?4))7W7unFyeVFfm2avRwKpv5imIgKI;69-U&1+1kv z!%abRnd!= zG}u2h2;bO1_q_w`Eq!Ula_+58qY>P=&<2CIHgHlhP1Y!poknwfSdb|T6Or>{@jOL~ zUIjiZq*FBdxk-GYf_=pIB$lw5MF+dWNM@;-<68ARQzGz?g^;)HAN%;BdONG)9yDkn zylB$G&%Li}XAR2fyD-gYUp-^B{UACJcpy9TZ{5b(RXlN8V%~iCxu?HgJ1a z2{S2{)s+6edqK|m(a$B7kZNcTX>H$76vP>!a0?8*eFJNQ5mu*w`g(epw|^ki6TXjC zqxA=AX9NAx&Ib9posDW*J&7g~D=A^D$Z;1%n9v~DTQ=X#X7oE=pEo;iq6qUm68M%Z zCc7JIgWF^DINzjgb*PQ9Va8J}6RRv_1Ic%|MYCkdlEA|J81M|fhtCsJWzhO4OtFiY zhZJhf?9^#!7>Feu{ zQKQc<2Zb4>7hmMh8yJe<#4*^lIu;xp90;#O+##G!*fj%Wxs2OoDltT`fX!Cp^H7WU zCaA^&Hphlb8J5L%VGRSqBnvktyLg;wQC1^{^FVMg#IB8Y1tVx6c@Sp2wSw!027;Ek zUthHQ4QP&wd847;DB9ZP-xyjPiILUzP%Jnw08K+PVM8?ZcSE6GwCmLi^A0pp4VrI( zEXd}t9u2c5noaw9VrYAYn->9CAMrWOU4s8+aT~}yxox(%g&(N|xROAlT^QMFavZjB zc32)QHl$(O;Bro4WC|#03gH69ri;*mKb`3vdYb76O5z>47h05Kq%B3>+5B81h?^K@^D`y66}w{ z@ClMh@M{Pfx(|YA3~w;pBYz}-5TK)!_-JTw5ayGph~uR}drEdL%>)xhdSEI-3nQ#W z1wsj-3}Qq#s4-+NY>orcHv|%pqXsCV59YMkYP3cU#JO2=St>znAOwKex^Q=>Pn2+= zZxE!Iup}s8QAep~TQKJw$=fhG7#tXk1^fF&4kVGtsyNJ)Hl#ZQ^0DuJR&RsIHjbd>zk2r4&1onOeV8_b~EnMP^Or&7`^$ylGT)a}OG zO{`Wa_EJ`3mrn*n*&twDyku8_HlX~BL{alJVXU;uDC}-|h6a0Lcd`cM`lhh-JVYCk zs5OryLH`X!mrC1XFtFKtI~t9=(pz22tnEBIQnA@eIIM9AKt*isoLKKFU zR77snNmyQ*C83JVH~`O)V9`h1+7U$fhlGBK<+P`Yxbm|LfHix_EU~qTW;E+KTr3!F zl6Vf=#)zk_WBR&o=3CcI|A8kou(A&nkXrHDH%4R43X!&`uj>Iw4=_4v69X_`>g&SH z!$*f}9hw?jhembQp=lk@I&Px}KWoGn?eXAuJ=*74M{xW^c=UStT^~k55hMg4AG%>q zH$v&qzD}$7$pwpvNcRaw?hW1_jScliaBlBrZDp~<*y2V7dj~_fC!1-n4`-fIW!d(@L%|%r-c5ira zwJa37tt@JEb>C2=8$b7oDHtl<)yK86D7(IVB59D2BrChMEE1QxJ)uxH%_CXYz+i~Rch9u^(JBaNnaNMXardfZ8EJRQ%&o*At?N=S-+m3 z$S@JkVfYF2V1(kG+q!=p706dU(h1!yWk&;_4PQo*OSm#&xOW%-pRi8 zVap>-dy(i^PgV)Rk3=xcZFivnaj%aM8g|b52=#mw|Au)icKCYUN9!Z&*8qX_OnZRo z53GcLP@o5T;2-eU18dkF5B&EmvRrGYD{lm2zKu|7HDZ<>>39cU1m+jrTWo02tK&sQ zF;hG{UOYXj`tBGgU+D|Uo38*>-lA%+gj?|y!Yx;xVv3@axiG$PN?i4l_x00bcCs6% zM6$0(SFBID`<)egq4d^U#j2ON%hvQj>CJ*>6=oeht3Y|KJ6i=R2dz~Pa5d+R0oGu7 z$u{>26+)ZiDC#T-F8Kk_WC4Vp;y^;NgXRw3$aKhH#v5^6EtNI4{O|E+1jqXTGY1t_ zp2*y-`2Xiy7Vw|26QD$Zd15!T-?^Fjgn0u9;|~rpEzVr=_%JvdOym;rr*M3^}-`B?6EllFobqHps}q8 zFaH%AR?ELD>Bnp^z2jCP^TdY>c>}7GNH9M*K@dj=a7<~r{Ksx~bDJ`fLCFS{DTm962iDu^C1 z$c{diTP}s!3t3DK)!JNWvxYY<5Y}4a{Ep z#Zxw~916c_z;3J@*BZ=b(Xb8K3SUqekM60Y<0*E%isRg|upT(FYgDALV&_j3s6~wj z-WY*qC$ms?HH2}$C1LCOC|g_-Hy4eM@y!@JZa}MO~O@tOL$QUI#~!? zqF#9vs8;RxPeCo=gJ8+pV7QkBisFU^qc4dBhkCnK!&=np9-6K+{ex;Woa2 zrEi`dN1FMv3tkx)jlgTY+RUmCH0h%CwAI z0Vxw}$C~426p|SUKL5~}24Y$-bfAd&e0+G!PU@jP1?u>bKkf?IQZSi@vXC!R$V+c7 zz6oSADPtoknAPgJe>}S^zBm=nkITqRwqS}hZJ4H(yepGf5`#7I5L5=W(ylRMc%w*2 z8VGB&{o+hZ6so(B6{N&IF*Gk3*h`3#^rM>13D(59wn)c}GC)7oTStM|hGagLXv<=KnhW&jTM77aF8P;y# z*C6kk;7K;w3k`iUXC_P-ACPEY|^5_#A>O zg4G#Oh7!*lx75fUnXz=hcr$CVfFw)_#tTI1VHq+${_}Vy(}tOQ7^YEHjV_=6S)=W@ zbOFA0!lE1Z!i~6n?mXJ z@WEd@tje6aVbGc4S<~o`L;IL)rZX%!w-$d{D7To~T!;3iP!5}e-3-g9C93j(dK%7x z%H?X{(mX27?PYOII(^#^&n*dC2XSHwWsB4leAX#ehvekc=s+DapbZw3fR?N_;@R%P zcvk9{(0_nqF9VnGWgv7FU6C}PkGN+r!(o5}tt>QuN=-iNv@wo99_q#WThLNf@^|i# z=eZFCEN9xJTW=Evv5<+)n>GvM{OKW5=fCm@F#*=1ZxBX-!jl|kn|F19#BY(#Dgi~4>5afh1YQY) zp)bczg!|Gm{mMz2_|Pu8M08sIErJSby)wDNTF^!UBuhP;1GhEzoe1GkFU{AsJqq~r!`&Q??LeMw2ZsE+f>L51{l;}2#)EAR`MVp43B!Q{BHHknE#VbTXw zyx+yD5`y_a@ktmYD4G-4?FI2%N3c^}f)`sB!Udv@%@n_a&M*4&I|v{m9KV`8Z;F$D zpFGp6Nhm+WHB$)$rEz&EsNCDJut_DGl_>iQTr-Qn{I*VA!ufNgk`hhFO-evKH)ah2 zMw1iHDOP?(3z=#GMg_u^0BKbZv1wEx`uM9N)+hn^1A}}^--Y)y-3ipled4z`nP#wh zV-zGJPSt$dfK?+v{H{|2`nentkl7Z5mr0JIMt}=ulc)+vVj*p7^nibrH7+O^wvJd? z9R$~+twZ_4P!R6NLlB|5AbW}9VWKK?z;w|~@hs;g;13APGjEt|3_8wDFfZ5PS&h); zN#eR_Rfd+85>RvKZ;)N~f(cM3lDSM8pBq8?H^Xo+0gb-=$9lTrCKAcPdd@(Tx=L1J{T!4gL8TsI;@@9ifvj=Ee&KKpfnalf+Yv+%JAEgFv2!X3{W$i{Tqnkqxno$l$OmdhKN4&3We8&>zn$KTznJ74K4LyB8<@>00SVTqrU<^3fUPG6nV-CQ z!=wsz**gm}DOkLN&|*Q0e%^#?ryiYAiB$k)S8D@-|C_9ky_ryB!GKxnl>pb+EP&*l z85V@Z8%IckS}+_>QI@j^QnDv7w=9^p1^g6MFnR0npSi6jA75%yB`iI4rW&$V+RZ9d zmbF0zlD175@HMpbk;t5(hD_?!u5zpBI*PC!K(cu}tlFB8b|7e!RfrSRZk`T9nP-}8 zTx%^^Enu{Ew*7L|$fNm6@oMx}){lJ@4JtBa!xb4z{9q)_RkP7!45wf%olsk$L4nij zPb!UsC<8+NhVWe5Uo3BHC<=EDaDU4R5Ni<$G4N__zeJQFle$Sc2Bof0!noY)=6aw4SZzS8~#6wAnZ8J5g9vk1VYqLoZM zDr%vy3Hl+d6qGra&x90{#X-d$SRo4kvWyiM23=2R6 z>l{H-!555-edzq1K1!qJQAJ_eqw|y3%yVUMn(qt8!h4>4z%=4v?NVJ7P*|LBShMb#-FN}pu+h0Bx>0fgyuTJ zsUZlrLqo-p=M=NZPn*=ri4k>I)VaRtYwL^ffa)5c`K%o`piY6M!9;-KU= z�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 literal 0 HcmV?d00001 diff --git a/v2/src-tauri/src/adb/driver.rs b/v2/src-tauri/src/adb/driver.rs index ce3a3af..f694129 100644 --- a/v2/src-tauri/src/adb/driver.rs +++ b/v2/src-tauri/src/adb/driver.rs @@ -94,6 +94,17 @@ pub trait AdbDriver: Send + Sync { "binary capture not supported by this driver", ))) } + + /// Spawn `adb ` as a long-lived child WITHOUT awaiting completion, + /// handing the caller the `Child` to own (configured `kill_on_drop(true)`). + /// Unlike `raw`/`shell`, the process is expected to keep running — for + /// resident helpers like the scrcpy control server. Default reports + /// unsupported so mocks need no extra wiring. + async fn spawn(&self, _args: &[&str]) -> AdbResult { + Err(AdbError::Io(std::io::Error::other( + "process spawn not supported by this driver", + ))) + } } /// The standard subprocess-backed driver. Wraps `tokio::process::Command`. @@ -242,6 +253,30 @@ impl AdbDriver for SubprocessAdb { Ok(output.stdout) } + + async fn spawn(&self, args: &[&str]) -> AdbResult { + if !self.binary.exists() { + return Err(AdbError::BinaryMissing { + path: self.binary.display().to_string(), + }); + } + + debug!(adb = ?self.binary, ?args, "adb spawn (long-lived)"); + + let mut cmd = Command::new(&self.binary); + cmd.args(args).kill_on_drop(true); + // Verified on device: the device-side `app_process` aborts at startup + // (exit 134, no Java output) when the adb client's stdin is fully + // *closed*. It needs an open fd — /dev/null works. A GUI app's inherited + // stdin is unreliable, so pin it to null explicitly. stdout/stderr are + // nulled too so the resident child can never block on a full pipe. + cmd.stdin(std::process::Stdio::null()) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()); + super::hide_console_window(&mut cmd); + + cmd.spawn().map_err(AdbError::Io) + } } /// Locate an adb binary by checking the standard installation locations. diff --git a/v2/src-tauri/src/adb/mod.rs b/v2/src-tauri/src/adb/mod.rs index b138045..72ab4c0 100644 --- a/v2/src-tauri/src/adb/mod.rs +++ b/v2/src-tauri/src/adb/mod.rs @@ -7,6 +7,7 @@ pub mod driver; pub mod install; pub mod parse; +pub mod remote_input; pub mod scan; use tokio::process::Command; @@ -38,4 +39,5 @@ pub use parse::{ parse_total_pss_by_process, parse_usage_stats, AppUsage, DisplayMode, FileEntry, RamInfo, StorageInfo, }; +pub use remote_input::RemoteInputSession; pub use scan::{local_subnet_prefix, scan_subnet, ScanHit}; diff --git a/v2/src-tauri/src/adb/remote_input.rs b/v2/src-tauri/src/adb/remote_input.rs new file mode 100644 index 0000000..f7c3b44 --- /dev/null +++ b/v2/src-tauri/src/adb/remote_input.rs @@ -0,0 +1,586 @@ +//! Persistent scrcpy control-socket session for low-latency remote input. +//! +//! Today each remote key press shells out `adb shell "input keyevent N"`, which +//! cold-starts a JVM on the TV (~690 ms per press). This module replaces the +//! transport with scrcpy's control-only server: push the jar once, run it +//! resident via `app_process`, and stream fixed binary control messages over a +//! forwarded TCP socket — dropping per-press cost to network RTT. +//! +//! Layering note: every adb invocation (push / forward / the `app_process` +//! spawn) goes through the `AdbDriver` trait, per the one-wrapper rule. The raw +//! `TcpStream` to the forwarded local port is a DOCUMENTED exception to that +//! rule — the same kind of exception `scan.rs` makes for its route/ip probes. +//! There is no way to carry scrcpy's binary control protocol over the driver's +//! line-oriented `shell`; the socket is the protocol. + +use std::path::Path; +use std::sync::atomic::{AtomicU32, Ordering}; +use std::sync::Arc; + +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::TcpStream; +use tokio::process::Child; +use tracing::{debug, warn}; + +use crate::adb::AdbDriver; + +/// The scrcpy protocol version this jar implements. MUST equal the version +/// baked into `resources/scrcpy-server-v3.1` or the server aborts on launch. +const SCRCPY_VERSION: &str = "3.1"; + +/// Where the server jar is pushed on the device. +const DEVICE_JAR_PATH: &str = "/data/local/tmp/shieldopt-scrcpy-server.jar"; + +/// Bundled jar location, relative to both the Tauri resource root (for +/// `BaseDirectory::Resource`) and the crate root (dev fallback). Resolved at +/// runtime by `commands::state::resolve_scrcpy_server_jar`. +pub const SERVER_JAR_RESOURCE_PATH: &str = "resources/scrcpy-server-v3.1"; + +/// scrcpy control message type bytes. +const TYPE_INJECT_KEYCODE: u8 = 0; +const TYPE_INJECT_TEXT: u8 = 1; + +/// Android `KeyEvent` actions. +const ACTION_DOWN: u8 = 0; +const ACTION_UP: u8 = 1; + +/// scrcpy caps a single INJECT_TEXT at 300 chars; longer strings are clamped. +const MAX_TEXT_CHARS: usize = 300; + +/// How many times to retry the TCP connect before giving up. With +/// `tunnel_forward=true` the server LISTENS on the localabstract socket and we +/// connect after `adb forward`, so there is a brief startup race. +const CONNECT_ATTEMPTS: usize = 10; +const CONNECT_RETRY_DELAY: std::time::Duration = std::time::Duration::from_millis(150); + +/// Encode an INJECT_KEYCODE control message (always 14 bytes, big-endian): +/// `[type=0][action][u32 keycode][u32 repeat][u32 metaState]`. +pub fn encode_inject_keycode(action: u8, keycode: u32, repeat: u32, meta_state: u32) -> Vec { + let mut buf = Vec::with_capacity(14); + buf.push(TYPE_INJECT_KEYCODE); + buf.push(action); + buf.extend_from_slice(&keycode.to_be_bytes()); + buf.extend_from_slice(&repeat.to_be_bytes()); + buf.extend_from_slice(&meta_state.to_be_bytes()); + buf +} + +/// Encode an INJECT_TEXT control message: `[type=1][u32 byte-len][UTF-8 bytes]`. +/// Text is clamped to 300 chars (scrcpy's cap) before encoding; the length +/// prefix is the UTF-8 byte length of the clamped string. +pub fn encode_inject_text(text: &str) -> Vec { + let clamped: String = text.chars().take(MAX_TEXT_CHARS).collect(); + let bytes = clamped.as_bytes(); + let mut buf = Vec::with_capacity(5 + bytes.len()); + buf.push(TYPE_INJECT_TEXT); + buf.extend_from_slice(&(bytes.len() as u32).to_be_bytes()); + buf.extend_from_slice(bytes); + buf +} + +/// Format a scid as scrcpy expects: 8 lowercase hex digits, masked to 31 bits +/// (the high bit must be clear — the server parses scid as a positive int and +/// treats a negative value as "no scid"). +fn format_scid(value: u32) -> String { + format!("{:08x}", value & 0x7fff_ffff) +} + +/// Process-wide counter mixed into the time seed so two sessions started within +/// the same millisecond still get distinct scids. +static SCID_COUNTER: AtomicU32 = AtomicU32::new(0); + +fn next_scid() -> String { + let nanos = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.subsec_nanos()) + .unwrap_or(0); + let bump = SCID_COUNTER.fetch_add(1, Ordering::Relaxed); + format_scid(nanos ^ bump.wrapping_mul(2654435761)) +} + +/// `adb -s forward tcp: localabstract:scrcpy_` argument +/// vector. The `-s` is load-bearing: without it, `adb forward` errors with +/// "more than one device/emulator" whenever a second device is connected. +fn forward_args(serial: &str, port: u16, scid: &str) -> Vec { + vec![ + "-s".to_string(), + serial.to_string(), + "forward".to_string(), + format!("tcp:{port}"), + format!("localabstract:scrcpy_{scid}"), + ] +} + +/// `adb -s forward --remove tcp:` argument vector. +fn forward_remove_args(serial: &str, port: u16) -> Vec { + vec![ + "-s".to_string(), + serial.to_string(), + "forward".to_string(), + "--remove".to_string(), + format!("tcp:{port}"), + ] +} + +/// `adb -s push ` argument vector. +fn push_args(serial: &str, local_jar: &str) -> Vec { + vec![ + "-s".to_string(), + serial.to_string(), + "push".to_string(), + local_jar.to_string(), + DEVICE_JAR_PATH.to_string(), + ] +} + +/// The full `adb` argument vector that launches the resident server: +/// `-s shell CLASSPATH= app_process / com.genymobile.scrcpy.Server scid=… …`. +/// Pure + deterministic given `serial`/`scid` so it can be asserted byte-for-byte. +fn server_spawn_args(serial: &str, scid: &str) -> Vec { + vec![ + "-s".to_string(), + serial.to_string(), + "shell".to_string(), + format!("CLASSPATH={DEVICE_JAR_PATH}"), + "app_process".to_string(), + "/".to_string(), + "com.genymobile.scrcpy.Server".to_string(), + SCRCPY_VERSION.to_string(), + format!("scid={scid}"), + "log_level=info".to_string(), + "video=false".to_string(), + "audio=false".to_string(), + "control=true".to_string(), + "tunnel_forward=true".to_string(), + "send_device_meta=false".to_string(), + "send_dummy_byte=true".to_string(), + ] +} + +/// Reserve a free local TCP port by binding to `:0` and reading back the +/// kernel-assigned port. There's a small TOCTOU window between drop and `adb +/// forward`, accepted as standard practice. +fn pick_free_local_port() -> Result { + let listener = std::net::TcpListener::bind("127.0.0.1:0") + .map_err(|e| format!("scrcpy: could not reserve a local port: {e}"))?; + let port = listener + .local_addr() + .map_err(|e| format!("scrcpy: could not read local port: {e}"))? + .port(); + Ok(port) +} + +/// Connect to the forwarded port AND complete the handshake, retrying the +/// whole sequence. The retry must cover the dummy-byte read, not just the +/// connect: with `adb forward`, the local adb daemon accepts the TCP +/// connection immediately even while the device-side socket isn't listening +/// yet, then closes it (EOF). Only a successful 0x00 read proves the server +/// is actually on the other end — same dance scrcpy's own client does. +async fn connect_with_retry(port: u16) -> Result { + let mut last_err = String::new(); + for attempt in 0..CONNECT_ATTEMPTS { + match TcpStream::connect(("127.0.0.1", port)).await { + Ok(mut stream) => { + let mut dummy = [0u8; 1]; + match tokio::time::timeout( + std::time::Duration::from_secs(2), + stream.read_exact(&mut dummy), + ) + .await + { + Ok(Ok(_)) if dummy[0] == 0x00 => return Ok(stream), + Ok(Ok(_)) => { + return Err(format!( + "scrcpy: unexpected handshake byte {:#04x} (expected 0x00)", + dummy[0] + )) + } + Ok(Err(e)) => last_err = format!("handshake read: {e}"), + Err(_) => last_err = "handshake read timed out".to_string(), + } + } + Err(e) => last_err = e.to_string(), + } + if attempt + 1 < CONNECT_ATTEMPTS { + tokio::time::sleep(CONNECT_RETRY_DELAY).await; + } + } + Err(format!( + "scrcpy: control channel never came up on 127.0.0.1:{port} after \ + {CONNECT_ATTEMPTS} attempts: {last_err}" + )) +} + +/// A live scrcpy control session bound to one device serial. Holds the open +/// control socket and the resident server child for the lifetime of the Remote +/// tab; the server exits the instant the socket closes, so the session must +/// keep both alive and tear down explicitly. +pub struct RemoteInputSession { + stream: TcpStream, + /// Kept alive for the session; `kill_on_drop(true)` reaps the server. + _child: Child, + scid: String, + port: u16, + serial: String, + /// Driver retained for async teardown (`adb forward --remove`). + adb: Arc, +} + +impl RemoteInputSession { + /// Push the server jar, forward a fresh local port to its control socket, + /// spawn the resident server, connect, and consume the handshake dummy byte. + pub async fn start( + adb: Arc, + jar_path: &Path, + serial: &str, + ) -> Result { + let local_jar = jar_path + .to_str() + .ok_or_else(|| "scrcpy: server jar path is not valid UTF-8".to_string())?; + + let push = push_args(serial, local_jar); + adb.raw_transfer(&as_str_args(&push)) + .await + .map_err(|e| format!("scrcpy: push server jar: {e}"))?; + + let scid = next_scid(); + let port = pick_free_local_port()?; + + let forward = forward_args(serial, port, &scid); + adb.raw(&as_str_args(&forward)) + .await + .map_err(|e| format!("scrcpy: adb forward tcp:{port}: {e}"))?; + + let spawn = server_spawn_args(serial, &scid); + let child = match adb.spawn(&as_str_args(&spawn)).await { + Ok(child) => child, + Err(e) => { + // Don't leak the forward we just created. + let _ = adb + .raw(&as_str_args(&forward_remove_args(serial, port))) + .await; + return Err(format!("scrcpy: spawn control server: {e}")); + } + }; + + let session = Self { + // Connect failure tears down explicitly (forward-remove + child + // kill) rather than leaking the resources start() just created. + stream: match connect_with_retry(port).await { + Ok(stream) => stream, + Err(e) => { + let mut child = child; + let _ = child.start_kill(); + let _ = adb + .raw(&as_str_args(&forward_remove_args(serial, port))) + .await; + return Err(e); + } + }, + _child: child, + scid, + port, + serial: serial.to_string(), + adb, + }; + + // The handshake dummy byte was already consumed inside + // connect_with_retry — the stream is ready for control messages. + debug!( + serial = %session.serial, + scid = %session.scid, + port = session.port, + "scrcpy control session established" + ); + Ok(session) + } + + /// Inject a single key-down or key-up event. + pub async fn send_key(&mut self, keycode: u32, down: bool) -> Result<(), String> { + let action = if down { ACTION_DOWN } else { ACTION_UP }; + let msg = encode_inject_keycode(action, keycode, 0, 0); + self.write_all(&msg).await + } + + /// Inject a full key press (down then up). + pub async fn send_key_press(&mut self, keycode: u32) -> Result<(), String> { + self.send_key(keycode, true).await?; + self.send_key(keycode, false).await + } + + /// Inject UTF-8 text (clamped to 300 chars). + pub async fn send_text(&mut self, text: &str) -> Result<(), String> { + let msg = encode_inject_text(text); + self.write_all(&msg).await + } + + async fn write_all(&mut self, bytes: &[u8]) -> Result<(), String> { + self.stream + .write_all(bytes) + .await + .map_err(|e| format!("scrcpy: control socket write failed: {e}"))?; + self.stream + .flush() + .await + .map_err(|e| format!("scrcpy: control socket flush failed: {e}")) + } + + /// Graceful teardown. Order matters (verified on device): close the control + /// socket FIRST — the server exits on its own the instant the socket drops — + /// then a belt-and-suspenders `pkill` for any lingering server process, then + /// reap the local child and remove the adb forward. `Drop` is the + /// best-effort backstop; prefer this. + pub async fn close(mut self) { + let _ = self.stream.shutdown().await; + // Match our jar's name, not "com.genymobile.scrcpy" — the broad pattern + // would also kill a desktop scrcpy session the user has open against + // this same device. + let _ = self + .adb + .shell(&self.serial, "pkill -f shieldopt-scrcpy-server") + .await; + let _ = self._child.start_kill(); + let _ = self + .adb + .raw(&as_str_args(&forward_remove_args(&self.serial, self.port))) + .await; + } +} + +impl Drop for RemoteInputSession { + fn drop(&mut self) { + // `_child` has kill_on_drop(true), so the server dies and the control + // socket closes here. Best-effort removal of the adb forward entry if a + // tokio runtime is live (e.g. app exit dropping the session registry). + let adb = self.adb.clone(); + let serial = self.serial.clone(); + let port = self.port; + if let Ok(handle) = tokio::runtime::Handle::try_current() { + handle.spawn(async move { + let args = forward_remove_args(&serial, port); + let _ = adb.raw(&as_str_args(&args)).await; + }); + } else { + warn!( + port, + "scrcpy: no runtime in Drop; adb forward entry may leak" + ); + } + } +} + +/// Borrow a `Vec` as the `&[&str]` the driver wants. +fn as_str_args(args: &[String]) -> Vec<&str> { + args.iter().map(String::as_str).collect() +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn inject_keycode_down_select_is_exact_14_bytes() { + // SELECT/center = 23, action down, repeat 0, meta 0. + let got = encode_inject_keycode(ACTION_DOWN, 23, 0, 0); + assert_eq!( + got, + vec![ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ] + ); + assert_eq!(got.len(), 14); + } + + #[test] + fn inject_keycode_up_dpad_with_repeat_and_meta() { + // D-pad up = 19 (0x13), action up = 1, repeat = 2, meta = 0x1001. + let got = encode_inject_keycode(ACTION_UP, 19, 2, 0x0000_1001); + assert_eq!( + got, + vec![ + 0x00, // type + 0x01, // action up + 0x00, 0x00, 0x00, 0x13, // keycode 19 + 0x00, 0x00, 0x00, 0x02, // repeat 2 + 0x00, 0x00, 0x10, 0x01, // meta + ] + ); + } + + #[test] + fn inject_text_encodes_type_len_then_utf8() { + let got = encode_inject_text("hi"); + assert_eq!(got, vec![0x01, 0x00, 0x00, 0x00, 0x02, b'h', b'i']); + } + + #[test] + fn inject_text_length_is_utf8_byte_count_not_char_count() { + // "é" is 2 UTF-8 bytes — the length prefix must be 2, not 1. + let got = encode_inject_text("é"); + assert_eq!(got[0], TYPE_INJECT_TEXT); + assert_eq!(&got[1..5], &[0x00, 0x00, 0x00, 0x02]); + assert_eq!(&got[5..], "é".as_bytes()); + } + + #[test] + fn inject_text_clamps_to_300_chars() { + let long = "a".repeat(500); + let got = encode_inject_text(&long); + // 1 type byte + 4 length bytes + 300 payload bytes. + assert_eq!(got.len(), 5 + 300); + assert_eq!(&got[1..5], &300u32.to_be_bytes()); + } + + #[test] + fn format_scid_is_8_hex_digits_with_high_bit_cleared() { + assert_eq!(format_scid(0), "00000000"); + assert_eq!(format_scid(0x0000_00ff), "000000ff"); + // High bit is masked off. + assert_eq!(format_scid(0xffff_ffff), "7fffffff"); + assert_eq!(format_scid(0x8000_0001), "00000001"); + } + + #[test] + fn server_spawn_args_match_phase0_launch_line() { + assert_eq!( + server_spawn_args("ABC123", "0a1b2c3d"), + vec![ + "-s", + "ABC123", + "shell", + "CLASSPATH=/data/local/tmp/shieldopt-scrcpy-server.jar", + "app_process", + "/", + "com.genymobile.scrcpy.Server", + "3.1", + "scid=0a1b2c3d", + "log_level=info", + "video=false", + "audio=false", + "control=true", + "tunnel_forward=true", + "send_device_meta=false", + "send_dummy_byte=true", + ] + ); + } + + #[test] + fn forward_and_push_args_are_well_formed() { + assert_eq!( + forward_args("ABC123", 41000, "0a1b2c3d"), + vec![ + "-s", + "ABC123", + "forward", + "tcp:41000", + "localabstract:scrcpy_0a1b2c3d" + ] + ); + assert_eq!( + forward_remove_args("ABC123", 41000), + vec!["-s", "ABC123", "forward", "--remove", "tcp:41000"] + ); + assert_eq!( + push_args("ABC123", "/tmp/scrcpy-server-v3.1"), + vec![ + "-s", + "ABC123", + "push", + "/tmp/scrcpy-server-v3.1", + "/data/local/tmp/shieldopt-scrcpy-server.jar", + ] + ); + } + + #[test] + fn pick_free_local_port_returns_a_usable_port() { + let port = pick_free_local_port().expect("should reserve a port"); + assert!(port > 0); + } + + // Honest error-path coverage: with a driver that can't spawn a child + // (MockAdb uses the trait's default `spawn`, which returns unsupported), + // start() must surface the spawn failure rather than hang or fake success. + // The live socket path is left for the lead's on-device check. + #[tokio::test] + async fn start_fails_when_driver_cannot_spawn() { + use crate::commands::test_support::MockAdb; + let adb: Arc = Arc::new(MockAdb::default()); + let jar = std::path::PathBuf::from("/tmp/scrcpy-server-v3.1"); + let result = RemoteInputSession::start(adb, &jar, "SERIAL123").await; + match result { + Ok(_) => panic!("start must fail when the driver cannot spawn the server"), + Err(err) => assert!( + err.contains("spawn control server"), + "unexpected error: {err}" + ), + } + } + + // Live end-to-end test against a real device — ignored in CI, run by hand: + // SHIELD_TEST_SERIAL=192.168.x.x:5555 cargo test remote_input_live -- --ignored --nocapture + // Starts a real session, injects SLEEP (223) and verifies via `dumpsys + // power` that the device went to sleep, then WAKEUP (224) and verifies it + // woke, then tears down and checks no server process is left behind. + #[tokio::test] + #[ignore] + async fn remote_input_live_roundtrip() { + let Ok(serial) = std::env::var("SHIELD_TEST_SERIAL") else { + panic!("set SHIELD_TEST_SERIAL to run this live test"); + }; + let adb: Arc = Arc::new( + crate::adb::SubprocessAdb::discover().expect("adb binary required for live test"), + ); + let jar = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join(super::SERVER_JAR_RESOURCE_PATH); + + async fn wakefulness(adb: &dyn AdbDriver, serial: &str) -> String { + adb.shell(serial, "dumpsys power | grep -m1 mWakefulness") + .await + .map(|o| o.stdout.trim().rsplit('=').next().unwrap_or("").to_string()) + .unwrap_or_default() + } + + let mut session = RemoteInputSession::start(adb.clone(), &jar, &serial) + .await + .expect("session start"); + + let t0 = std::time::Instant::now(); + session.send_key_press(223).await.expect("inject SLEEP"); + let mut state = String::new(); + for _ in 0..40 { + state = wakefulness(adb.as_ref(), &serial).await; + if state == "Asleep" { + break; + } + } + println!( + "SLEEP -> {state} in {:?} (incl. dumpsys polling)", + t0.elapsed() + ); + assert_eq!(state, "Asleep", "device should have gone to sleep"); + + tokio::time::sleep(std::time::Duration::from_millis(800)).await; + session.send_key_press(224).await.expect("inject WAKEUP"); + tokio::time::sleep(std::time::Duration::from_millis(1500)).await; + assert_eq!( + wakefulness(adb.as_ref(), &serial).await, + "Awake", + "device should have woken" + ); + + session.close().await; + tokio::time::sleep(std::time::Duration::from_millis(800)).await; + let leftovers = adb + .shell(&serial, "ps -A | grep shieldopt-scrcpy || true") + .await + .map(|o| o.stdout.trim().to_string()) + .unwrap_or_default(); + assert!( + leftovers.is_empty(), + "server process left behind after close(): {leftovers}" + ); + } +} diff --git a/v2/src-tauri/src/commands/devices.rs b/v2/src-tauri/src/commands/devices.rs index 4e15cd9..20a0459 100644 --- a/v2/src-tauri/src/commands/devices.rs +++ b/v2/src-tauri/src/commands/devices.rs @@ -137,6 +137,9 @@ pub async fn disconnect_device( state: State<'_, AppState>, serial: String, ) -> Result { + // A live remote-input session holds an open socket + forward to this + // device — tear it down before dropping the connection. + state.drop_remote_session(&serial).await; let adb = state.adb_snapshot().await; let out = adb .raw(&["disconnect", &serial]) diff --git a/v2/src-tauri/src/commands/input.rs b/v2/src-tauri/src/commands/input.rs index 1931f84..d76d4df 100644 --- a/v2/src-tauri/src/commands/input.rs +++ b/v2/src-tauri/src/commands/input.rs @@ -1,15 +1,37 @@ -//! Send text to the TV — `input text` over ADB, for typing Wi-Fi passwords -//! and searches from a real keyboard instead of the on-screen D-pad grid. +//! Remote input — keys and text to the TV. +//! +//! Two transports, tried in order: +//! 1. The persistent scrcpy control channel (`adb::remote_input`) — per-press +//! cost is network RTT (~ms). Lazily started on first use; full UTF-8 text. +//! 2. `adb shell input …` — the slow (~690 ms/press, ASCII-only) but +//! universally-available fallback when the channel can't start or its +//! socket dies. A failed channel send drops the session so the next press +//! retries a fresh start. use serde::Serialize; use tauri::State; +use super::state::resolve_scrcpy_server_jar; use super::AppState; #[derive(Serialize)] pub struct SendTextResult { pub ok: bool, pub message: String, + /// Which transport served this request: "channel" (scrcpy control socket) + /// or "shell" (legacy `input` fallback). The Remote tab shows a live cue. + pub transport: &'static str, +} + +/// Make sure the scrcpy session for `serial` is up (starting it if needed). +async fn channel_ready( + state: &AppState, + app: &tauri::AppHandle, + serial: &str, +) -> Result<(), String> { + let jar = resolve_scrcpy_server_jar(app)?; + let adb = state.adb_snapshot().await; + state.ensure_remote_session(adb, &jar, serial).await } const MAX_TEXT_LEN: usize = 500; @@ -42,15 +64,53 @@ fn encode_input_text(text: &str) -> Result { } /// `send_text` — type `text` into whatever input field has focus on the TV. +/// Channel first (full UTF-8, instant); `input text` (ASCII-only) fallback. #[tauri::command] pub async fn send_text( + app: tauri::AppHandle, state: State<'_, AppState>, serial: String, text: String, ) -> Result { + if text.is_empty() { + return Ok(SendTextResult { + ok: false, + message: "Nothing to send.".to_string(), + transport: "none", + }); + } + + let channel = match channel_ready(&state, &app, &serial).await { + Ok(()) => match state.remote_send_text(&serial, &text).await { + Ok(()) => Ok(()), + Err(e) => { + state.drop_remote_session(&serial).await; + Err(e) + } + }, + Err(e) => Err(e), + }; + if channel.is_ok() { + return Ok(SendTextResult { + ok: true, + message: format!( + "Sent {} character(s) to the focused field.", + text.chars().count() + ), + transport: "channel", + }); + } + tracing::warn!(error = ?channel, %serial, "scrcpy channel unavailable; using input text"); + let encoded = match encode_input_text(&text) { Ok(e) => e, - Err(message) => return Ok(SendTextResult { ok: false, message }), + Err(message) => { + return Ok(SendTextResult { + ok: false, + message, + transport: "shell", + }) + } }; let adb = state.adb_snapshot().await; let out = adb @@ -63,11 +123,13 @@ pub async fn send_text( Ok(SendTextResult { ok: true, message: format!("Sent {} character(s) to the focused field.", text.len()), + transport: "shell", }) } else { Ok(SendTextResult { ok: false, message: noise, + transport: "shell", }) } } @@ -100,10 +162,12 @@ fn keycode_for(key: &str) -> Option { }) } -/// `send_key` — one remote button press (`input keyevent `). Used by -/// the Remote panel's D-pad and by live typing for Backspace/Enter. +/// `send_key` — one remote button press. Channel first (instant, real +/// down/up); `input keyevent` fallback. Used by the Remote panel's D-pad and +/// by live typing for Backspace/Enter. #[tauri::command] pub async fn send_key( + app: tauri::AppHandle, state: State<'_, AppState>, serial: String, key: String, @@ -111,6 +175,26 @@ pub async fn send_key( let Some(code) = keycode_for(&key) else { return Err(format!("Unknown remote key: {key:?}")); }; + + let channel = match channel_ready(&state, &app, &serial).await { + Ok(()) => match state.remote_send_key_press(&serial, code).await { + Ok(()) => Ok(()), + Err(e) => { + state.drop_remote_session(&serial).await; + Err(e) + } + }, + Err(e) => Err(e), + }; + if channel.is_ok() { + return Ok(SendTextResult { + ok: true, + message: format!("Sent {key}."), + transport: "channel", + }); + } + tracing::warn!(error = ?channel, %serial, "scrcpy channel unavailable; using input keyevent"); + let adb = state.adb_snapshot().await; let out = adb .shell(&serial, &format!("input keyevent {code}")) @@ -124,6 +208,7 @@ pub async fn send_key( } else { noise }, + transport: "shell", }) } diff --git a/v2/src-tauri/src/commands/state.rs b/v2/src-tauri/src/commands/state.rs index 93c0f91..321a85d 100644 --- a/v2/src-tauri/src/commands/state.rs +++ b/v2/src-tauri/src/commands/state.rs @@ -1,13 +1,14 @@ //! Shared application state held across Tauri command invocations. use std::collections::HashMap; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::Arc; -use tokio::sync::RwLock; +use tokio::sync::{Mutex, RwLock}; use crate::adb::driver::discover_adb_binary; -use crate::adb::{AdbDriver, AdbError, AdbOutput, AdbResult, SubprocessAdb}; +use crate::adb::remote_input::SERVER_JAR_RESOURCE_PATH; +use crate::adb::{AdbDriver, AdbError, AdbOutput, AdbResult, RemoteInputSession, SubprocessAdb}; use crate::engine::AppListBundle; /// State managed by Tauri's state store. Held by `tauri::Builder::manage`. @@ -28,6 +29,9 @@ pub struct AppState { /// ID. There's no cheap way to read an app's label over adb, so this is a /// curated map loaded from `data/app-lists/known-names.json`. pub known_names: HashMap, + /// Live scrcpy control sessions, keyed by device serial. Lazily started on + /// the first remote key and held open for the Remote tab's lifetime. + pub remote_sessions: Mutex>, } impl AppState { @@ -38,6 +42,7 @@ impl AppState { snapshot_dir: data_dir.join("snapshots"), data_dir, known_names: HashMap::new(), + remote_sessions: Mutex::new(HashMap::new()), } } @@ -76,6 +81,100 @@ impl AppState { pub async fn replace_adb(&self, new_driver: Arc) { *self.adb.write().await = new_driver; } + + /// Get-or-start the scrcpy control session for `serial`. The slow `start()` + /// (push + forward + spawn + connect) runs OUTSIDE the registry lock so a + /// cold start can't block other commands; the lock is only taken for the + /// fast presence check and the final insert. If two callers race, the loser + /// tears its extra session down. + pub async fn ensure_remote_session( + &self, + adb: Arc, + jar_path: &Path, + serial: &str, + ) -> Result<(), String> { + if self.remote_sessions.lock().await.contains_key(serial) { + return Ok(()); + } + let session = RemoteInputSession::start(adb, jar_path, serial).await?; + let mut guard = self.remote_sessions.lock().await; + if guard.contains_key(serial) { + drop(guard); + session.close().await; + } else { + guard.insert(serial.to_string(), session); + } + Ok(()) + } + + /// Inject a single key-down / key-up via the live session. Errors if no + /// session exists — Phase 3 calls `ensure_remote_session` first, and on a + /// write error should `drop_remote_session` and fall back to `input`. + pub async fn remote_send_key( + &self, + serial: &str, + keycode: u32, + down: bool, + ) -> Result<(), String> { + let mut guard = self.remote_sessions.lock().await; + let session = guard + .get_mut(serial) + .ok_or_else(|| "no active remote session".to_string())?; + session.send_key(keycode, down).await + } + + /// Inject a full key press (down + up) via the live session. + pub async fn remote_send_key_press(&self, serial: &str, keycode: u32) -> Result<(), String> { + let mut guard = self.remote_sessions.lock().await; + let session = guard + .get_mut(serial) + .ok_or_else(|| "no active remote session".to_string())?; + session.send_key_press(keycode).await + } + + /// Inject UTF-8 text via the live session. + pub async fn remote_send_text(&self, serial: &str, text: &str) -> Result<(), String> { + let mut guard = self.remote_sessions.lock().await; + let session = guard + .get_mut(serial) + .ok_or_else(|| "no active remote session".to_string())?; + session.send_text(text).await + } + + /// Tear down and forget the session for `serial`, if any. Removes it from + /// the registry first, then closes outside the lock. + pub async fn drop_remote_session(&self, serial: &str) { + let session = self.remote_sessions.lock().await.remove(serial); + if let Some(session) = session { + session.close().await; + } + } +} + +/// Resolve the on-disk path of the bundled scrcpy server jar. Prefers the +/// Tauri resource directory (production install); falls back to the +/// crate-relative `resources/` dir for `cargo run` / `cargo test`. +/// +/// Phase 3: the remote-input command calls this with its `AppHandle` to get the +/// jar path, then hands it to `AppState::ensure_remote_session`. +pub fn resolve_scrcpy_server_jar(app: &tauri::AppHandle) -> Result { + use tauri::Manager; + if let Ok(p) = app.path().resolve( + SERVER_JAR_RESOURCE_PATH, + tauri::path::BaseDirectory::Resource, + ) { + if p.is_file() { + return Ok(p); + } + } + let dev = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(SERVER_JAR_RESOURCE_PATH); + if dev.is_file() { + return Ok(dev); + } + Err(format!( + "scrcpy server jar not found (looked in the Tauri resource dir and {})", + dev.display() + )) } /// Driver used when no adb binary could be discovered at startup. Every call diff --git a/v2/src-tauri/tauri.conf.json b/v2/src-tauri/tauri.conf.json index e5bead6..2ae87fd 100644 --- a/v2/src-tauri/tauri.conf.json +++ b/v2/src-tauri/tauri.conf.json @@ -45,6 +45,9 @@ "icons/icon.icns", "icons/icon.ico" ], + "resources": [ + "resources/scrcpy-server-v3.1" + ], "windows": { "wix": { "version": "2.0.314" diff --git a/v2/src/lib/components/RemoteTab.svelte b/v2/src/lib/components/RemoteTab.svelte index 566798d..b100305 100644 --- a/v2/src/lib/components/RemoteTab.svelte +++ b/v2/src/lib/components/RemoteTab.svelte @@ -8,12 +8,20 @@ let remoteEcho = $state(""); let remoteMessage = $state(""); let remoteCaptureFocused = $state(false); + /// Transport the backend last reported: "channel" = instant scrcpy socket, + /// "shell" = slow `input` fallback (~700 ms/press), null until the first + /// send. Drives the status cue, typing batch window, and hold-to-repeat. + let transport = $state<"channel" | "shell" | null>(null); // Keystrokes are sent strictly in order through one promise chain — a // backspace must never overtake the characters typed before it. let remoteQueue: Promise = Promise.resolve(); let remoteBuffer = ""; let remoteFlushTimer: ReturnType | null = null; + function noteTransport(t: string) { + if (t === "channel" || t === "shell") transport = t; + } + function remoteEnqueue(work: () => Promise) { remoteQueue = remoteQueue.then(work).catch((e) => { remoteMessage = String(e); @@ -30,6 +38,7 @@ remoteBuffer = ""; remoteEnqueue(async () => { const r = await api.sendText(serial, chunk); + noteTransport(r.transport); if (!r.ok) remoteMessage = r.message; }); } @@ -38,10 +47,31 @@ remoteFlushBuffer(); remoteEnqueue(async () => { const r = await api.sendKey(serial, key); + noteTransport(r.transport); remoteMessage = r.ok ? "" : r.message; }); } + // Hold-to-repeat for the D-pad: first repeat after 400 ms, then ~7/s. Only + // armed on the instant channel — on the slow shell transport the queue + // would pile up far behind the finger, so a hold is just one press there. + let repeatTimer: ReturnType | null = null; + let repeatInterval: ReturnType | null = null; + + function stopRepeat() { + if (repeatTimer) { clearTimeout(repeatTimer); repeatTimer = null; } + if (repeatInterval) { clearInterval(repeatInterval); repeatInterval = null; } + } + + function pressStart(key: string) { + sendRemoteKey(key); + if (transport !== "channel") return; + stopRepeat(); + repeatTimer = setTimeout(() => { + repeatInterval = setInterval(() => sendRemoteKey(key), 140); + }, 400); + } + function remoteKeydown(e: KeyboardEvent) { if (e.metaKey || e.ctrlKey || e.altKey) return; if (e.key === "Backspace") { @@ -56,32 +86,44 @@ sendRemoteKey("enter"); return; } - if (e.key.length === 1 && e.key >= " " && e.key <= "~") { + if (e.key.length === 1 && !e.isComposing) { + // The channel injects full UTF-8; the shell fallback re-checks and + // rejects non-ASCII with a clear message, so don't pre-filter here. e.preventDefault(); remoteBuffer += e.key; remoteEcho = (remoteEcho + e.key).slice(-60); - // Batch rapid typing into one `input text` per pause — each adb call - // is a ~100-300 ms round-trip, so per-character would lag behind fast - // typists forever. + // Channel sends are ~ms, so flush almost immediately — characters land + // on the TV as you type. The shell fallback pays ~700 ms per call, so + // batch rapid typing into one `input text` per pause instead. if (remoteFlushTimer) clearTimeout(remoteFlushTimer); - remoteFlushTimer = setTimeout(remoteFlushBuffer, 250); + remoteFlushTimer = setTimeout(remoteFlushBuffer, transport === "shell" ? 250 : 30); } } onDestroy(() => { if (remoteFlushTimer) clearTimeout(remoteFlushTimer); + stopRepeat(); });
-

Remote

+
+

Remote

+ {#if transport} + + {transport === "channel" ? "● instant" : "○ compatible (slower)"} + + {/if} +

Live typing

Click below and type — keystrokes go straight to whatever field has - focus on the TV, including Backspace and Enter. Each press is an ADB - round-trip, so it feels like typing over SSH. + focus on the TV, including Backspace and Enter.

Buttons

+
- + - + - + - +
@@ -153,6 +198,19 @@ margin: 0 0 0.8rem; font-size: 1.1rem; } + .remote-header { + display: flex; + align-items: baseline; + gap: 0.8rem; + } + .transport { + font-size: 0.74rem; + color: var(--fg-muted); + cursor: default; + } + .transport.live { + color: var(--ok); + } .card h3 { margin: 1rem 0 0.4rem; font-size: 1rem; diff --git a/v2/src/lib/types.ts b/v2/src/lib/types.ts index 254940d..291a029 100644 --- a/v2/src/lib/types.ts +++ b/v2/src/lib/types.ts @@ -208,6 +208,9 @@ export interface ScreenshotResult { export interface SendTextResult { ok: boolean; message: string; + /// "channel" = scrcpy control socket (instant), "shell" = legacy `input` + /// fallback (~700 ms/press), "none" = nothing was sent. + transport: "channel" | "shell" | "none"; } export interface FileEntry { From b69787c880002d6db6ba9df74f3882a1ab557c19 Mon Sep 17 00:00:00 2001 From: Bryan Roscoe Date: Sun, 14 Jun 2026 22:50:17 -0500 Subject: [PATCH 2/7] Remote: add Menu and Recents buttons (double-tap Home for recents on most Android TVs) --- v2/package-lock.json | 4 ++-- v2/src-tauri/src/commands/apps.rs | 12 ++++++++++-- v2/src-tauri/src/commands/input.rs | 4 ++++ v2/src/lib/components/RemoteTab.svelte | 4 ++++ 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/v2/package-lock.json b/v2/package-lock.json index 38e0847..2f4b7f9 100644 --- a/v2/package-lock.json +++ b/v2/package-lock.json @@ -1,12 +1,12 @@ { "name": "shield-optimizer-v2", - "version": "2.0.0-beta.13", + "version": "2.0.0-beta.14", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "shield-optimizer-v2", - "version": "2.0.0-beta.13", + "version": "2.0.0-beta.14", "license": "MIT", "dependencies": { "@tauri-apps/api": "^2", diff --git a/v2/src-tauri/src/commands/apps.rs b/v2/src-tauri/src/commands/apps.rs index ce1b338..02616d2 100644 --- a/v2/src-tauri/src/commands/apps.rs +++ b/v2/src-tauri/src/commands/apps.rs @@ -414,7 +414,12 @@ pub async fn set_app_op_impl( }); } let mode = if allow { "allow" } else { "deny" }; - run(state, serial, &format!("cmd appops set {package} {op} {mode}")).await + run( + state, + serial, + &format!("cmd appops set {package} {op} {mode}"), + ) + .await } /// `get_app_op` — reads the current appops mode for ` `. @@ -435,7 +440,10 @@ pub async fn get_app_op_impl( package: &str, op: &str, ) -> Result { - if !is_valid_package_name(package) || !op.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') || op.is_empty() { + if !is_valid_package_name(package) + || !op.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') + || op.is_empty() + { return Ok("missing".to_string()); } let adb = state.adb_snapshot().await; diff --git a/v2/src-tauri/src/commands/input.rs b/v2/src-tauri/src/commands/input.rs index d76d4df..df9525b 100644 --- a/v2/src-tauri/src/commands/input.rs +++ b/v2/src-tauri/src/commands/input.rs @@ -145,6 +145,8 @@ fn keycode_for(key: &str) -> Option { "select" => 23, "back" => 4, "home" => 3, + "menu" => 82, + "recents" => 187, "play_pause" => 85, "rewind" => 89, "fast_forward" => 90, @@ -245,6 +247,8 @@ mod tests { assert_eq!(keycode_for("up"), Some(19)); assert_eq!(keycode_for("select"), Some(23)); assert_eq!(keycode_for("back"), Some(4)); + assert_eq!(keycode_for("menu"), Some(82)); + assert_eq!(keycode_for("recents"), Some(187)); assert_eq!(keycode_for("delete"), Some(67)); assert_eq!(keycode_for("wakeup"), Some(224)); assert_eq!(keycode_for("power"), Some(26)); diff --git a/v2/src/lib/components/RemoteTab.svelte b/v2/src/lib/components/RemoteTab.svelte index b100305..bea466e 100644 --- a/v2/src/lib/components/RemoteTab.svelte +++ b/v2/src/lib/components/RemoteTab.svelte @@ -166,6 +166,10 @@
+ +
+
+
From a4a8d6b464ba32785338c22cf72ef8086329233f Mon Sep 17 00:00:00 2001 From: Bryan Roscoe Date: Sun, 14 Jun 2026 22:51:20 -0500 Subject: [PATCH 3/7] Detect mDNS wireless-debugging serials as Network, not USB --- v2/src-tauri/src/adb/parse.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/v2/src-tauri/src/adb/parse.rs b/v2/src-tauri/src/adb/parse.rs index b103acf..dcbf97b 100644 --- a/v2/src-tauri/src/adb/parse.rs +++ b/v2/src-tauri/src/adb/parse.rs @@ -48,7 +48,11 @@ pub fn parse_device_list(adb_devices_output: &str) -> Vec { let Some(status) = DeviceStatus::from_adb_str(status_str) else { continue; }; - let connection = if IP_PORT.is_match(serial) { + // Network if it's an `ip:port` serial OR an mDNS wireless-debugging + // serial (Android 11+ pairs over `_adb-tls-connect._tcp` etc.; those + // never look like `ip:port` but always carry the `_tcp` service tag). + // USB serials are plain hardware ids and contain neither. + let connection = if IP_PORT.is_match(serial) || serial.contains("._tcp") { ConnectionType::Network } else { ConnectionType::Usb @@ -534,6 +538,17 @@ mod tests { assert_eq!(entries[3].status, DeviceStatus::Offline); } + #[test] + fn mdns_wireless_debugging_serial_is_network_not_usb() { + // Android 11+ wireless debugging registers over mDNS; the serial is the + // service name, not an ip:port. It must still classify as Network. + let input = "List of devices attached\n\ + adb-58040DLCH005YV-jBeCEe._adb-tls-connect._tcp\tdevice\n"; + let entries = parse_device_list(input); + assert_eq!(entries.len(), 1); + assert_eq!(entries[0].connection, ConnectionType::Network); + } + #[test] fn ignores_header_and_blank_lines() { let input = "List of devices attached\n\n\n"; From a5005a0bc2ff909f35609bd0b23919c84f9a67f0 Mon Sep 17 00:00:00 2001 From: Bryan Roscoe Date: Sun, 14 Jun 2026 22:53:55 -0500 Subject: [PATCH 4/7] Detect device type by TV characteristic, not brand alone (Pixel phones no longer mislabeled Google TV) --- v2/src-tauri/src/commands/devices.rs | 3 +- v2/src-tauri/src/engine/detection.rs | 74 +++++++++++++++++++++++++--- v2/src-tauri/src/engine/types.rs | 5 ++ v2/src/lib/types.ts | 1 + 4 files changed, 74 insertions(+), 9 deletions(-) diff --git a/v2/src-tauri/src/commands/devices.rs b/v2/src-tauri/src/commands/devices.rs index 20a0459..9ea9232 100644 --- a/v2/src-tauri/src/commands/devices.rs +++ b/v2/src-tauri/src/commands/devices.rs @@ -252,7 +252,7 @@ async fn harvest_properties(adb: &dyn AdbDriver, serial: &str) -> DeviceProperti getprop ro.product.model; getprop ro.product.device; \ getprop ro.product.manufacturer; getprop ro.build.version.release; \ getprop ro.build.version.sdk; getprop ro.build.id; \ - getprop ro.board.platform"; + getprop ro.board.platform; getprop ro.build.characteristics"; let Ok(out) = adb.shell(serial, cmd).await else { return DeviceProperties::default(); @@ -288,6 +288,7 @@ async fn harvest_properties(adb: &dyn AdbDriver, serial: &str) -> DeviceProperti sdk_level: get(6), build_id: get(7), board_platform: get(8), + characteristics: get(9), } } diff --git a/v2/src-tauri/src/engine/detection.rs b/v2/src-tauri/src/engine/detection.rs index 266ec38..a326d82 100644 --- a/v2/src-tauri/src/engine/detection.rs +++ b/v2/src-tauri/src/engine/detection.rs @@ -37,8 +37,17 @@ pub fn detect_device_type(props: &DeviceProperties) -> DeviceType { let model = props.model.to_ascii_lowercase(); let device = props.device_codename.to_ascii_lowercase(); let manufacturer = props.manufacturer.to_ascii_lowercase(); - - // Shield: any signal from Nvidia or known Shield codenames. + // `ro.build.characteristics` carries `tv` on Android TV / Google TV. It is + // the signal that separates a TV from a phone or tablet sharing the same + // brand — a Google Pixel is `brand == "google"` too, so brand alone is not + // enough to call something a TV. + let is_tv = props + .characteristics + .split(',') + .any(|c| c.trim().eq_ignore_ascii_case("tv")); + + // Shield: any signal from Nvidia or known Shield codenames. Shield boxes + // are never phones, so these strong signals don't need the `tv` gate. if brand == "nvidia" || manufacturer == "nvidia" || model.contains("shield") @@ -47,13 +56,10 @@ pub fn detect_device_type(props: &DeviceProperties) -> DeviceType { return DeviceType::Shield; } - // Google TV: Onn (Walmart), Google-branded, or device codename matching - // known Google TV products. Amlogic-based Onn boxes (`ott_...`) and the - // newer Chromecast / Streamer codenames (`sabrina`, `boreal`) belong here. + // Google TV by strong product-specific signals: Onn (Walmart) boxes + // (`ott_...`), Chromecast, and the newer Streamer codenames are TV-only + // products, so they stand on their own. if brand == "onn" - || brand == "google" - || manufacturer == "google" - || manufacturer == "amlogic" || model.contains("onn") || model.contains("chromecast") || model.contains("sabrina") @@ -64,6 +70,19 @@ pub fn detect_device_type(props: &DeviceProperties) -> DeviceType { return DeviceType::GoogleTv; } + // Google / Amlogic branding only means Google TV when the device actually + // reports the `tv` characteristic — otherwise it's a phone or tablet (e.g. + // a Google Pixel) that happens to share the brand. + if is_tv && (brand == "google" || manufacturer == "google" || manufacturer == "amlogic") { + return DeviceType::GoogleTv; + } + + // Any other device that reports itself as a TV: classify as Google TV so it + // gets the Android-TV app list rather than nothing. + if is_tv { + return DeviceType::GoogleTv; + } + DeviceType::Unknown } @@ -93,6 +112,19 @@ mod tests { } } + fn props_ch( + brand: &str, + model: &str, + device: &str, + manufacturer: &str, + characteristics: &str, + ) -> DeviceProperties { + DeviceProperties { + characteristics: characteristics.to_string(), + ..props(brand, model, device, manufacturer) + } + } + #[test] fn detects_shield_by_brand() { assert_eq!( @@ -134,6 +166,32 @@ mod tests { ); } + #[test] + fn google_branded_phone_is_not_google_tv() { + // A Google Pixel phone shares brand/manufacturer "google" but reports + // no `tv` characteristic — it must not be classified as Google TV. + assert_eq!( + detect_device_type(&props_ch("google", "Pixel 10 Pro", "blazer", "Google", "")), + DeviceType::Unknown + ); + } + + #[test] + fn google_branded_tv_with_tv_characteristic_is_google_tv() { + assert_eq!( + detect_device_type(&props_ch("google", "Some TV", "generic", "Google", "tv")), + DeviceType::GoogleTv + ); + } + + #[test] + fn generic_box_reporting_tv_characteristic_is_google_tv() { + assert_eq!( + detect_device_type(&props_ch("Generic", "TV Box", "rk3328", "Generic", "tv")), + DeviceType::GoogleTv + ); + } + #[test] fn unknown_when_no_signals() { assert_eq!( diff --git a/v2/src-tauri/src/engine/types.rs b/v2/src-tauri/src/engine/types.rs index 09fc162..016921d 100644 --- a/v2/src-tauri/src/engine/types.rs +++ b/v2/src-tauri/src/engine/types.rs @@ -55,6 +55,11 @@ pub struct DeviceProperties { pub build_id: String, /// `getprop ro.board.platform`. pub board_platform: String, + /// `getprop ro.build.characteristics` — comma-separated list that includes + /// `tv` on Android TV / Google TV devices. The signal that distinguishes a + /// TV from a phone or tablet sharing the same brand (e.g. Google Pixel). + #[serde(default)] + pub characteristics: String, } /// A connected device — what the device list shows and what every action targets. diff --git a/v2/src/lib/types.ts b/v2/src/lib/types.ts index 291a029..47113a9 100644 --- a/v2/src/lib/types.ts +++ b/v2/src/lib/types.ts @@ -17,6 +17,7 @@ export interface DeviceProperties { sdk_level: string; build_id: string; board_platform: string; + characteristics?: string; } export interface Device { From 178eb7613156c5193a32426304af3f8c66ef944f Mon Sep 17 00:00:00 2001 From: Bryan Roscoe Date: Sun, 14 Jun 2026 23:05:33 -0500 Subject: [PATCH 5/7] Remote: add 'Force compatible mode' toggle to bypass the fast channel --- v2/src-tauri/src/commands/input.rs | 58 +++++++++++++++++--------- v2/src/lib/api.ts | 8 ++-- v2/src/lib/components/RemoteTab.svelte | 36 ++++++++++++++-- v2/src/lib/prefs.ts | 15 +++++++ 4 files changed, 90 insertions(+), 27 deletions(-) diff --git a/v2/src-tauri/src/commands/input.rs b/v2/src-tauri/src/commands/input.rs index df9525b..feddbc1 100644 --- a/v2/src-tauri/src/commands/input.rs +++ b/v2/src-tauri/src/commands/input.rs @@ -71,6 +71,10 @@ pub async fn send_text( state: State<'_, AppState>, serial: String, text: String, + // When true, skip the fast channel and use `input text` directly — the + // Remote tab's "Force compatible mode" escape hatch for devices where the + // channel starts but misbehaves. + force_shell: bool, ) -> Result { if text.is_empty() { return Ok(SendTextResult { @@ -80,15 +84,19 @@ pub async fn send_text( }); } - let channel = match channel_ready(&state, &app, &serial).await { - Ok(()) => match state.remote_send_text(&serial, &text).await { - Ok(()) => Ok(()), - Err(e) => { - state.drop_remote_session(&serial).await; - Err(e) - } - }, - Err(e) => Err(e), + let channel = if force_shell { + Err("compatibility mode forced".to_string()) + } else { + match channel_ready(&state, &app, &serial).await { + Ok(()) => match state.remote_send_text(&serial, &text).await { + Ok(()) => Ok(()), + Err(e) => { + state.drop_remote_session(&serial).await; + Err(e) + } + }, + Err(e) => Err(e), + } }; if channel.is_ok() { return Ok(SendTextResult { @@ -100,7 +108,9 @@ pub async fn send_text( transport: "channel", }); } - tracing::warn!(error = ?channel, %serial, "scrcpy channel unavailable; using input text"); + if !force_shell { + tracing::warn!(error = ?channel, %serial, "scrcpy channel unavailable; using input text"); + } let encoded = match encode_input_text(&text) { Ok(e) => e, @@ -173,20 +183,26 @@ pub async fn send_key( state: State<'_, AppState>, serial: String, key: String, + // See `send_text` — forces the slow `input keyevent` path. + force_shell: bool, ) -> Result { let Some(code) = keycode_for(&key) else { return Err(format!("Unknown remote key: {key:?}")); }; - let channel = match channel_ready(&state, &app, &serial).await { - Ok(()) => match state.remote_send_key_press(&serial, code).await { - Ok(()) => Ok(()), - Err(e) => { - state.drop_remote_session(&serial).await; - Err(e) - } - }, - Err(e) => Err(e), + let channel = if force_shell { + Err("compatibility mode forced".to_string()) + } else { + match channel_ready(&state, &app, &serial).await { + Ok(()) => match state.remote_send_key_press(&serial, code).await { + Ok(()) => Ok(()), + Err(e) => { + state.drop_remote_session(&serial).await; + Err(e) + } + }, + Err(e) => Err(e), + } }; if channel.is_ok() { return Ok(SendTextResult { @@ -195,7 +211,9 @@ pub async fn send_key( transport: "channel", }); } - tracing::warn!(error = ?channel, %serial, "scrcpy channel unavailable; using input keyevent"); + if !force_shell { + tracing::warn!(error = ?channel, %serial, "scrcpy channel unavailable; using input keyevent"); + } let adb = state.adb_snapshot().await; let out = adb diff --git a/v2/src/lib/api.ts b/v2/src/lib/api.ts index 89270fd..aa1decc 100644 --- a/v2/src/lib/api.ts +++ b/v2/src/lib/api.ts @@ -128,10 +128,10 @@ export const api = { invoke>("app_usage_map", { serial }), safetyInfo: (pkg: string) => invoke("safety_info", { package: pkg }), trimCaches: (serial: string) => invoke("trim_caches", { serial }), - sendText: (serial: string, text: string) => - invoke("send_text", { serial, text }), - sendKey: (serial: string, key: string) => - invoke("send_key", { serial, key }), + sendText: (serial: string, text: string, forceShell = false) => + invoke("send_text", { serial, text, forceShell }), + sendKey: (serial: string, key: string, forceShell = false) => + invoke("send_key", { serial, key, forceShell }), installApk: (serial: string, apkPath: string, reinstall = true) => invoke("install_apk", { serial, apkPath, reinstall }), diff --git a/v2/src/lib/components/RemoteTab.svelte b/v2/src/lib/components/RemoteTab.svelte index bea466e..a95bba4 100644 --- a/v2/src/lib/components/RemoteTab.svelte +++ b/v2/src/lib/components/RemoteTab.svelte @@ -1,9 +1,21 @@