99import android .view .accessibility .AccessibilityNodeInfo ;
1010import android .view .accessibility .AccessibilityNodeInfo .AccessibilityAction ;
1111import android .view .accessibility .AccessibilityWindowInfo ;
12+ import java .io .BufferedReader ;
1213import java .io .File ;
1314import java .io .FileOutputStream ;
1415import java .io .IOException ;
16+ import java .io .InputStreamReader ;
17+ import java .io .OutputStream ;
18+ import java .net .InetAddress ;
19+ import java .net .ServerSocket ;
20+ import java .net .Socket ;
1521import java .lang .reflect .Field ;
1622import java .nio .charset .StandardCharsets ;
1723import java .util .List ;
@@ -51,17 +57,19 @@ public void onStart() {
5157 int maxNodes = readIntArgument (arguments , "maxNodes" , DEFAULT_MAX_NODES );
5258 String outputPath = readStringArgument (arguments , "outputPath" );
5359 boolean emitChunks = readBooleanArgument (arguments , "emitChunks" , true );
60+ int sessionPort = readIntArgument (arguments , "sessionPort" , 0 );
5461 Bundle result = new Bundle ();
55- result .putString ("agentDeviceProtocol" , PROTOCOL );
56- result .putString ("helperApiVersion" , HELPER_API_VERSION );
57- result .putString ("outputFormat" , OUTPUT_FORMAT );
58- result .putString ("waitForIdleTimeoutMs" , Long .toString (waitForIdleTimeoutMs ));
59- result .putString ("waitForIdleQuietMs" , Long .toString (waitForIdleQuietMs ));
60- result .putString ("timeoutMs" , Long .toString (timeoutMs ));
61- result .putString ("maxDepth" , Integer .toString (maxDepth ));
62- result .putString ("maxNodes" , Integer .toString (maxNodes ));
62+ putBaseMetadata (result , waitForIdleTimeoutMs , waitForIdleQuietMs , timeoutMs , maxDepth , maxNodes );
6363
6464 try {
65+ if (sessionPort > 0 ) {
66+ runSnapshotSession (
67+ sessionPort , waitForIdleQuietMs , waitForIdleTimeoutMs , timeoutMs , maxDepth , maxNodes );
68+ result .putString ("ok" , "true" );
69+ result .putString ("sessionEnded" , "true" );
70+ finishSafely (0 , result );
71+ return ;
72+ }
6573 long startedAtMs = System .currentTimeMillis ();
6674 CaptureResult capture =
6775 captureXml (waitForIdleQuietMs , waitForIdleTimeoutMs , timeoutMs , maxDepth , maxNodes );
@@ -70,12 +78,7 @@ public void onStart() {
7078 emitChunks (capture .xml );
7179 }
7280 result .putString ("ok" , "true" );
73- result .putString ("rootPresent" , Boolean .toString (capture .rootPresent ));
74- result .putString ("captureMode" , capture .captureMode );
75- result .putString ("windowCount" , Integer .toString (capture .windowCount ));
76- result .putString ("nodeCount" , Integer .toString (capture .nodeCount ));
77- result .putString ("truncated" , Boolean .toString (capture .truncated ));
78- result .putString ("elapsedMs" , Long .toString (System .currentTimeMillis () - startedAtMs ));
81+ putCaptureMetadata (result , capture , System .currentTimeMillis () - startedAtMs );
7982 finishSafely (0 , result );
8083 } catch (Throwable error ) {
8184 result .putString ("ok" , "false" );
@@ -87,6 +90,158 @@ public void onStart() {
8790 }
8891 }
8992
93+ private static void putBaseMetadata (
94+ Bundle result ,
95+ long waitForIdleTimeoutMs ,
96+ long waitForIdleQuietMs ,
97+ long timeoutMs ,
98+ int maxDepth ,
99+ int maxNodes ) {
100+ result .putString ("agentDeviceProtocol" , PROTOCOL );
101+ result .putString ("helperApiVersion" , HELPER_API_VERSION );
102+ result .putString ("outputFormat" , OUTPUT_FORMAT );
103+ result .putString ("waitForIdleTimeoutMs" , Long .toString (waitForIdleTimeoutMs ));
104+ result .putString ("waitForIdleQuietMs" , Long .toString (waitForIdleQuietMs ));
105+ result .putString ("timeoutMs" , Long .toString (timeoutMs ));
106+ result .putString ("maxDepth" , Integer .toString (maxDepth ));
107+ result .putString ("maxNodes" , Integer .toString (maxNodes ));
108+ }
109+
110+ private static void putCaptureMetadata (Bundle result , CaptureResult capture , long elapsedMs ) {
111+ result .putString ("rootPresent" , Boolean .toString (capture .rootPresent ));
112+ result .putString ("captureMode" , capture .captureMode );
113+ result .putString ("windowCount" , Integer .toString (capture .windowCount ));
114+ result .putString ("nodeCount" , Integer .toString (capture .nodeCount ));
115+ result .putString ("truncated" , Boolean .toString (capture .truncated ));
116+ result .putString ("elapsedMs" , Long .toString (elapsedMs ));
117+ }
118+
119+ private void runSnapshotSession (
120+ int sessionPort ,
121+ long waitForIdleQuietMs ,
122+ long waitForIdleTimeoutMs ,
123+ long timeoutMs ,
124+ int maxDepth ,
125+ int maxNodes )
126+ throws IOException {
127+ try (ServerSocket server =
128+ new ServerSocket (sessionPort , 1 , InetAddress .getByName ("127.0.0.1" ))) {
129+ Bundle ready = new Bundle ();
130+ putBaseMetadata (
131+ ready , waitForIdleTimeoutMs , waitForIdleQuietMs , timeoutMs , maxDepth , maxNodes );
132+ ready .putString ("sessionReady" , "true" );
133+ ready .putString ("sessionPort" , Integer .toString (sessionPort ));
134+ sendStatus (2 , ready );
135+
136+ while (!Thread .currentThread ().isInterrupted ()) {
137+ try (Socket socket = server .accept ()) {
138+ String command =
139+ new BufferedReader (
140+ new InputStreamReader (socket .getInputStream (), StandardCharsets .UTF_8 ))
141+ .readLine ();
142+ if (command == null ) {
143+ writeSessionError (socket .getOutputStream (), "" , "java.io.EOFException" , "empty command" );
144+ continue ;
145+ }
146+ String [] parts = command .trim ().split ("\\ s+" , 2 );
147+ String action = parts .length > 0 ? parts [0 ] : "" ;
148+ String requestId = parts .length > 1 ? parts [1 ] : "" ;
149+ if ("quit" .equals (action )) {
150+ writeSessionOk (socket .getOutputStream (), requestId );
151+ return ;
152+ }
153+ if (!"snapshot" .equals (action )) {
154+ writeSessionError (
155+ socket .getOutputStream (),
156+ requestId ,
157+ "java.lang.IllegalArgumentException" ,
158+ "unknown session command" );
159+ continue ;
160+ }
161+ writeSessionSnapshot (
162+ socket .getOutputStream (),
163+ requestId ,
164+ waitForIdleQuietMs ,
165+ waitForIdleTimeoutMs ,
166+ timeoutMs ,
167+ maxDepth ,
168+ maxNodes );
169+ }
170+ }
171+ }
172+ }
173+
174+ private void writeSessionSnapshot (
175+ OutputStream output ,
176+ String requestId ,
177+ long waitForIdleQuietMs ,
178+ long waitForIdleTimeoutMs ,
179+ long timeoutMs ,
180+ int maxDepth ,
181+ int maxNodes )
182+ throws IOException {
183+ Bundle result = new Bundle ();
184+ putBaseMetadata (result , waitForIdleTimeoutMs , waitForIdleQuietMs , timeoutMs , maxDepth , maxNodes );
185+ result .putString ("requestId" , requestId );
186+ try {
187+ long startedAtMs = System .currentTimeMillis ();
188+ CaptureResult capture =
189+ captureXml (waitForIdleQuietMs , waitForIdleTimeoutMs , timeoutMs , maxDepth , maxNodes );
190+ result .putString ("ok" , "true" );
191+ putCaptureMetadata (result , capture , System .currentTimeMillis () - startedAtMs );
192+ result .putString ("byteLength" , Integer .toString (capture .xml .getBytes (StandardCharsets .UTF_8 ).length ));
193+ writeSessionResponse (output , result , capture .xml );
194+ } catch (Throwable error ) {
195+ writeSessionError (
196+ output ,
197+ requestId ,
198+ error .getClass ().getName (),
199+ error .getMessage () == null ? error .getClass ().getName () : error .getMessage ());
200+ }
201+ }
202+
203+ private static void writeSessionOk (OutputStream output , String requestId ) throws IOException {
204+ Bundle result = new Bundle ();
205+ result .putString ("agentDeviceProtocol" , PROTOCOL );
206+ result .putString ("helperApiVersion" , HELPER_API_VERSION );
207+ result .putString ("outputFormat" , OUTPUT_FORMAT );
208+ result .putString ("requestId" , requestId );
209+ result .putString ("ok" , "true" );
210+ writeSessionResponse (output , result , "" );
211+ }
212+
213+ private static void writeSessionError (
214+ OutputStream output , String requestId , String errorType , String message ) throws IOException {
215+ Bundle result = new Bundle ();
216+ result .putString ("agentDeviceProtocol" , PROTOCOL );
217+ result .putString ("helperApiVersion" , HELPER_API_VERSION );
218+ result .putString ("outputFormat" , OUTPUT_FORMAT );
219+ result .putString ("requestId" , requestId );
220+ result .putString ("ok" , "false" );
221+ result .putString ("errorType" , errorType );
222+ result .putString ("message" , message );
223+ writeSessionResponse (output , result , "" );
224+ }
225+
226+ private static void writeSessionResponse (OutputStream output , Bundle result , String body )
227+ throws IOException {
228+ StringBuilder headers = new StringBuilder ();
229+ for (String key : result .keySet ()) {
230+ Object value = result .get (key );
231+ if (value != null ) {
232+ headers .append (key ).append ('=' ).append (sanitizeHeaderValue (value .toString ())).append ('\n' );
233+ }
234+ }
235+ headers .append ('\n' );
236+ output .write (headers .toString ().getBytes (StandardCharsets .UTF_8 ));
237+ output .write (body .getBytes (StandardCharsets .UTF_8 ));
238+ output .flush ();
239+ }
240+
241+ private static String sanitizeHeaderValue (String value ) {
242+ return value .replace ('\r' , ' ' ).replace ('\n' , ' ' );
243+ }
244+
90245 private static String readStringArgument (Bundle arguments , String key ) {
91246 if (arguments == null || !arguments .containsKey (key )) {
92247 return null ;
0 commit comments