react-native-ai-devtools 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/LICENSE +32 -0
  2. package/README.md +1250 -0
  3. package/build/__tests__/helpers/fake-cdp-server.d.ts +56 -0
  4. package/build/__tests__/helpers/fake-cdp-server.d.ts.map +1 -0
  5. package/build/__tests__/helpers/fake-cdp-server.js +108 -0
  6. package/build/__tests__/helpers/fake-cdp-server.js.map +1 -0
  7. package/build/__tests__/integration/connection-health.test.d.ts +2 -0
  8. package/build/__tests__/integration/connection-health.test.d.ts.map +1 -0
  9. package/build/__tests__/integration/connection-health.test.js +151 -0
  10. package/build/__tests__/integration/connection-health.test.js.map +1 -0
  11. package/build/__tests__/integration/execute-in-app.test.d.ts +2 -0
  12. package/build/__tests__/integration/execute-in-app.test.d.ts.map +1 -0
  13. package/build/__tests__/integration/execute-in-app.test.js +115 -0
  14. package/build/__tests__/integration/execute-in-app.test.js.map +1 -0
  15. package/build/__tests__/integration/tools.test.d.ts +2 -0
  16. package/build/__tests__/integration/tools.test.d.ts.map +1 -0
  17. package/build/__tests__/integration/tools.test.js +228 -0
  18. package/build/__tests__/integration/tools.test.js.map +1 -0
  19. package/build/__tests__/setup.d.ts +2 -0
  20. package/build/__tests__/setup.d.ts.map +1 -0
  21. package/build/__tests__/setup.js +11 -0
  22. package/build/__tests__/setup.js.map +1 -0
  23. package/build/__tests__/unit/bundle.test.d.ts +2 -0
  24. package/build/__tests__/unit/bundle.test.d.ts.map +1 -0
  25. package/build/__tests__/unit/bundle.test.js +53 -0
  26. package/build/__tests__/unit/bundle.test.js.map +1 -0
  27. package/build/__tests__/unit/connection-health.test.d.ts +2 -0
  28. package/build/__tests__/unit/connection-health.test.d.ts.map +1 -0
  29. package/build/__tests__/unit/connection-health.test.js +28 -0
  30. package/build/__tests__/unit/connection-health.test.js.map +1 -0
  31. package/build/__tests__/unit/executor.test.d.ts +2 -0
  32. package/build/__tests__/unit/executor.test.d.ts.map +1 -0
  33. package/build/__tests__/unit/executor.test.js +79 -0
  34. package/build/__tests__/unit/executor.test.js.map +1 -0
  35. package/build/__tests__/unit/logs.test.d.ts +2 -0
  36. package/build/__tests__/unit/logs.test.d.ts.map +1 -0
  37. package/build/__tests__/unit/logs.test.js +81 -0
  38. package/build/__tests__/unit/logs.test.js.map +1 -0
  39. package/build/__tests__/unit/metro.test.d.ts +2 -0
  40. package/build/__tests__/unit/metro.test.d.ts.map +1 -0
  41. package/build/__tests__/unit/metro.test.js +61 -0
  42. package/build/__tests__/unit/metro.test.js.map +1 -0
  43. package/build/__tests__/unit/network.test.d.ts +2 -0
  44. package/build/__tests__/unit/network.test.d.ts.map +1 -0
  45. package/build/__tests__/unit/network.test.js +102 -0
  46. package/build/__tests__/unit/network.test.js.map +1 -0
  47. package/build/__tests__/unit/tap.test.d.ts +2 -0
  48. package/build/__tests__/unit/tap.test.d.ts.map +1 -0
  49. package/build/__tests__/unit/tap.test.js +157 -0
  50. package/build/__tests__/unit/tap.test.js.map +1 -0
  51. package/build/core/android.d.ts +265 -0
  52. package/build/core/android.d.ts.map +1 -0
  53. package/build/core/android.js +1413 -0
  54. package/build/core/android.js.map +1 -0
  55. package/build/core/bundle.d.ts +49 -0
  56. package/build/core/bundle.d.ts.map +1 -0
  57. package/build/core/bundle.js +368 -0
  58. package/build/core/bundle.js.map +1 -0
  59. package/build/core/connection.d.ts +43 -0
  60. package/build/core/connection.d.ts.map +1 -0
  61. package/build/core/connection.js +963 -0
  62. package/build/core/connection.js.map +1 -0
  63. package/build/core/connectionState.d.ts +108 -0
  64. package/build/core/connectionState.d.ts.map +1 -0
  65. package/build/core/connectionState.js +284 -0
  66. package/build/core/connectionState.js.map +1 -0
  67. package/build/core/errorScreenParser.d.ts +30 -0
  68. package/build/core/errorScreenParser.d.ts.map +1 -0
  69. package/build/core/errorScreenParser.js +198 -0
  70. package/build/core/errorScreenParser.js.map +1 -0
  71. package/build/core/executor.d.ts +113 -0
  72. package/build/core/executor.d.ts.map +1 -0
  73. package/build/core/executor.js +1877 -0
  74. package/build/core/executor.js.map +1 -0
  75. package/build/core/format.d.ts +8 -0
  76. package/build/core/format.d.ts.map +1 -0
  77. package/build/core/format.js +34 -0
  78. package/build/core/format.js.map +1 -0
  79. package/build/core/guides.d.ts +14 -0
  80. package/build/core/guides.d.ts.map +1 -0
  81. package/build/core/guides.js +261 -0
  82. package/build/core/guides.js.map +1 -0
  83. package/build/core/httpServer.d.ts +14 -0
  84. package/build/core/httpServer.d.ts.map +1 -0
  85. package/build/core/httpServer.js +2459 -0
  86. package/build/core/httpServer.js.map +1 -0
  87. package/build/core/httpServerProcess.d.ts +25 -0
  88. package/build/core/httpServerProcess.d.ts.map +1 -0
  89. package/build/core/httpServerProcess.js +153 -0
  90. package/build/core/httpServerProcess.js.map +1 -0
  91. package/build/core/index.d.ts +25 -0
  92. package/build/core/index.d.ts.map +1 -0
  93. package/build/core/index.js +53 -0
  94. package/build/core/index.js.map +1 -0
  95. package/build/core/ios.d.ts +214 -0
  96. package/build/core/ios.d.ts.map +1 -0
  97. package/build/core/ios.js +1232 -0
  98. package/build/core/ios.js.map +1 -0
  99. package/build/core/logs.d.ts +43 -0
  100. package/build/core/logs.d.ts.map +1 -0
  101. package/build/core/logs.js +144 -0
  102. package/build/core/logs.js.map +1 -0
  103. package/build/core/metro.d.ts +23 -0
  104. package/build/core/metro.d.ts.map +1 -0
  105. package/build/core/metro.js +96 -0
  106. package/build/core/metro.js.map +1 -0
  107. package/build/core/network.d.ts +43 -0
  108. package/build/core/network.d.ts.map +1 -0
  109. package/build/core/network.js +217 -0
  110. package/build/core/network.js.map +1 -0
  111. package/build/core/networkInterceptor.d.ts +3 -0
  112. package/build/core/networkInterceptor.d.ts.map +1 -0
  113. package/build/core/networkInterceptor.js +203 -0
  114. package/build/core/networkInterceptor.js.map +1 -0
  115. package/build/core/ocr.d.ts +69 -0
  116. package/build/core/ocr.d.ts.map +1 -0
  117. package/build/core/ocr.js +212 -0
  118. package/build/core/ocr.js.map +1 -0
  119. package/build/core/state.d.ts +17 -0
  120. package/build/core/state.d.ts.map +1 -0
  121. package/build/core/state.js +50 -0
  122. package/build/core/state.js.map +1 -0
  123. package/build/core/tap.d.ts +91 -0
  124. package/build/core/tap.d.ts.map +1 -0
  125. package/build/core/tap.js +542 -0
  126. package/build/core/tap.js.map +1 -0
  127. package/build/core/telemetry.d.ts +4 -0
  128. package/build/core/telemetry.d.ts.map +1 -0
  129. package/build/core/telemetry.js +289 -0
  130. package/build/core/telemetry.js.map +1 -0
  131. package/build/core/types.d.ts +134 -0
  132. package/build/core/types.d.ts.map +1 -0
  133. package/build/core/types.js +2 -0
  134. package/build/core/types.js.map +1 -0
  135. package/build/httpServerStandalone.d.ts +7 -0
  136. package/build/httpServerStandalone.d.ts.map +1 -0
  137. package/build/httpServerStandalone.js +31 -0
  138. package/build/httpServerStandalone.js.map +1 -0
  139. package/build/index.d.ts +3 -0
  140. package/build/index.d.ts.map +1 -0
  141. package/build/index.js +3012 -0
  142. package/build/index.js.map +1 -0
  143. package/build/pro/tap.d.ts +91 -0
  144. package/build/pro/tap.d.ts.map +1 -0
  145. package/build/pro/tap.js +542 -0
  146. package/build/pro/tap.js.map +1 -0
  147. package/package.json +63 -0
@@ -0,0 +1,1413 @@
1
+ import { exec } from "child_process";
2
+ import { promisify } from "util";
3
+ import { existsSync } from "fs";
4
+ import path from "path";
5
+ import os from "os";
6
+ import sharp from "sharp";
7
+ const execAsync = promisify(exec);
8
+ // XML parsing for uiautomator dump
9
+ import { XMLParser } from "fast-xml-parser";
10
+ // ADB command timeout in milliseconds
11
+ const ADB_TIMEOUT = 30000;
12
+ /**
13
+ * Check if ADB is available in PATH
14
+ */
15
+ export async function isAdbAvailable() {
16
+ try {
17
+ await execAsync("adb version", { timeout: 5000 });
18
+ return true;
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ }
24
+ /**
25
+ * List connected Android devices
26
+ */
27
+ export async function listAndroidDevices() {
28
+ try {
29
+ const adbAvailable = await isAdbAvailable();
30
+ if (!adbAvailable) {
31
+ return {
32
+ success: false,
33
+ error: "ADB is not installed or not in PATH. Install Android SDK Platform Tools."
34
+ };
35
+ }
36
+ const { stdout } = await execAsync("adb devices -l", { timeout: ADB_TIMEOUT });
37
+ const lines = stdout.trim().split("\n");
38
+ // Skip the "List of devices attached" header
39
+ const deviceLines = lines.slice(1).filter((line) => line.trim().length > 0);
40
+ if (deviceLines.length === 0) {
41
+ return {
42
+ success: true,
43
+ result: "No Android devices connected."
44
+ };
45
+ }
46
+ const devices = deviceLines.map((line) => {
47
+ const parts = line.trim().split(/\s+/);
48
+ const id = parts[0];
49
+ const status = parts[1];
50
+ const device = { id, status };
51
+ // Parse additional info like product:xxx model:xxx device:xxx transport_id:xxx
52
+ for (let i = 2; i < parts.length; i++) {
53
+ const [key, value] = parts[i].split(":");
54
+ if (key === "product")
55
+ device.product = value;
56
+ else if (key === "model")
57
+ device.model = value;
58
+ else if (key === "device")
59
+ device.device = value;
60
+ else if (key === "transport_id")
61
+ device.transportId = value;
62
+ }
63
+ return device;
64
+ });
65
+ const formatted = devices
66
+ .map((d) => {
67
+ let info = `${d.id} (${d.status})`;
68
+ if (d.model)
69
+ info += ` - ${d.model.replace(/_/g, " ")}`;
70
+ if (d.product)
71
+ info += ` [${d.product}]`;
72
+ return info;
73
+ })
74
+ .join("\n");
75
+ return {
76
+ success: true,
77
+ result: `Connected Android devices:\n${formatted}`,
78
+ devices
79
+ };
80
+ }
81
+ catch (error) {
82
+ return {
83
+ success: false,
84
+ error: `Failed to list devices: ${error instanceof Error ? error.message : String(error)}`
85
+ };
86
+ }
87
+ }
88
+ /**
89
+ * Get the first connected Android device ID
90
+ */
91
+ export async function getDefaultAndroidDevice() {
92
+ try {
93
+ const { stdout } = await execAsync("adb devices", { timeout: ADB_TIMEOUT });
94
+ const lines = stdout.trim().split("\n");
95
+ const deviceLines = lines.slice(1).filter((line) => line.trim().length > 0);
96
+ for (const line of deviceLines) {
97
+ const [id, status] = line.trim().split(/\s+/);
98
+ if (status === "device") {
99
+ return id;
100
+ }
101
+ }
102
+ return null;
103
+ }
104
+ catch {
105
+ return null;
106
+ }
107
+ }
108
+ /**
109
+ * Build device selector for ADB command
110
+ */
111
+ function buildDeviceArg(deviceId) {
112
+ return deviceId ? `-s ${deviceId}` : "";
113
+ }
114
+ /**
115
+ * Take a screenshot from an Android device
116
+ */
117
+ export async function androidScreenshot(outputPath, deviceId) {
118
+ try {
119
+ const adbAvailable = await isAdbAvailable();
120
+ if (!adbAvailable) {
121
+ return {
122
+ success: false,
123
+ error: "ADB is not installed or not in PATH. Install Android SDK Platform Tools."
124
+ };
125
+ }
126
+ const deviceArg = buildDeviceArg(deviceId);
127
+ const device = deviceId || (await getDefaultAndroidDevice());
128
+ if (!device) {
129
+ return {
130
+ success: false,
131
+ error: "No Android device connected. Connect a device or start an emulator."
132
+ };
133
+ }
134
+ // Generate output path if not provided
135
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
136
+ const finalOutputPath = outputPath || path.join(os.tmpdir(), `android-screenshot-${timestamp}.png`);
137
+ // Capture screenshot on device
138
+ const remotePath = "/sdcard/screenshot-temp.png";
139
+ await execAsync(`adb ${deviceArg} shell screencap -p ${remotePath}`, {
140
+ timeout: ADB_TIMEOUT
141
+ });
142
+ // Pull screenshot to local machine
143
+ await execAsync(`adb ${deviceArg} pull ${remotePath} "${finalOutputPath}"`, {
144
+ timeout: ADB_TIMEOUT
145
+ });
146
+ // Clean up remote file
147
+ await execAsync(`adb ${deviceArg} shell rm ${remotePath}`, {
148
+ timeout: ADB_TIMEOUT
149
+ }).catch(() => {
150
+ // Ignore cleanup errors
151
+ });
152
+ // Resize image if needed (API limit: 2000px max for multi-image requests)
153
+ // Return scale factor so AI can convert image coords to device coords
154
+ const MAX_DIMENSION = 2000;
155
+ const image = sharp(finalOutputPath);
156
+ const metadata = await image.metadata();
157
+ const originalWidth = metadata.width || 0;
158
+ const originalHeight = metadata.height || 0;
159
+ let imageData;
160
+ let scaleFactor = 1;
161
+ if (originalWidth > MAX_DIMENSION || originalHeight > MAX_DIMENSION) {
162
+ // Calculate scale to fit within MAX_DIMENSION
163
+ scaleFactor = Math.max(originalWidth, originalHeight) / MAX_DIMENSION;
164
+ imageData = await image
165
+ .resize(MAX_DIMENSION, MAX_DIMENSION, {
166
+ fit: "inside",
167
+ withoutEnlargement: true
168
+ })
169
+ .jpeg({ quality: 85 })
170
+ .toBuffer();
171
+ }
172
+ else {
173
+ imageData = await image
174
+ .jpeg({ quality: 85 })
175
+ .toBuffer();
176
+ }
177
+ return {
178
+ success: true,
179
+ result: finalOutputPath,
180
+ data: imageData,
181
+ scaleFactor,
182
+ originalWidth,
183
+ originalHeight
184
+ };
185
+ }
186
+ catch (error) {
187
+ return {
188
+ success: false,
189
+ error: `Failed to capture screenshot: ${error instanceof Error ? error.message : String(error)}`
190
+ };
191
+ }
192
+ }
193
+ /**
194
+ * Install an APK on an Android device
195
+ */
196
+ export async function androidInstallApp(apkPath, deviceId, options) {
197
+ try {
198
+ const adbAvailable = await isAdbAvailable();
199
+ if (!adbAvailable) {
200
+ return {
201
+ success: false,
202
+ error: "ADB is not installed or not in PATH. Install Android SDK Platform Tools."
203
+ };
204
+ }
205
+ // Verify APK exists
206
+ if (!existsSync(apkPath)) {
207
+ return {
208
+ success: false,
209
+ error: `APK file not found: ${apkPath}`
210
+ };
211
+ }
212
+ const deviceArg = buildDeviceArg(deviceId);
213
+ const device = deviceId || (await getDefaultAndroidDevice());
214
+ if (!device) {
215
+ return {
216
+ success: false,
217
+ error: "No Android device connected. Connect a device or start an emulator."
218
+ };
219
+ }
220
+ // Build install flags
221
+ const flags = [];
222
+ if (options?.replace)
223
+ flags.push("-r");
224
+ if (options?.grantPermissions)
225
+ flags.push("-g");
226
+ const flagsStr = flags.length > 0 ? flags.join(" ") + " " : "";
227
+ const { stdout, stderr } = await execAsync(`adb ${deviceArg} install ${flagsStr}"${apkPath}"`, { timeout: 120000 } // 2 minute timeout for install
228
+ );
229
+ const output = stdout + stderr;
230
+ if (output.includes("Success")) {
231
+ return {
232
+ success: true,
233
+ result: `Successfully installed ${path.basename(apkPath)}`
234
+ };
235
+ }
236
+ else {
237
+ return {
238
+ success: false,
239
+ error: output.trim() || "Installation failed with unknown error"
240
+ };
241
+ }
242
+ }
243
+ catch (error) {
244
+ return {
245
+ success: false,
246
+ error: `Failed to install app: ${error instanceof Error ? error.message : String(error)}`
247
+ };
248
+ }
249
+ }
250
+ /**
251
+ * Launch an app on an Android device
252
+ */
253
+ export async function androidLaunchApp(packageName, activityName, deviceId) {
254
+ try {
255
+ const adbAvailable = await isAdbAvailable();
256
+ if (!adbAvailable) {
257
+ return {
258
+ success: false,
259
+ error: "ADB is not installed or not in PATH. Install Android SDK Platform Tools."
260
+ };
261
+ }
262
+ const deviceArg = buildDeviceArg(deviceId);
263
+ const device = deviceId || (await getDefaultAndroidDevice());
264
+ if (!device) {
265
+ return {
266
+ success: false,
267
+ error: "No Android device connected. Connect a device or start an emulator."
268
+ };
269
+ }
270
+ let command;
271
+ if (activityName) {
272
+ // Launch specific activity
273
+ command = `adb ${deviceArg} shell am start -n ${packageName}/${activityName}`;
274
+ }
275
+ else {
276
+ // Launch main/launcher activity
277
+ command = `adb ${deviceArg} shell monkey -p ${packageName} -c android.intent.category.LAUNCHER 1`;
278
+ }
279
+ const { stdout, stderr } = await execAsync(command, { timeout: ADB_TIMEOUT });
280
+ const output = stdout + stderr;
281
+ // Check for errors
282
+ if (output.includes("Error") || output.includes("Exception")) {
283
+ return {
284
+ success: false,
285
+ error: output.trim()
286
+ };
287
+ }
288
+ return {
289
+ success: true,
290
+ result: `Launched ${packageName}${activityName ? `/${activityName}` : ""}`
291
+ };
292
+ }
293
+ catch (error) {
294
+ return {
295
+ success: false,
296
+ error: `Failed to launch app: ${error instanceof Error ? error.message : String(error)}`
297
+ };
298
+ }
299
+ }
300
+ /**
301
+ * Get list of installed packages on the device
302
+ */
303
+ export async function androidListPackages(deviceId, filter) {
304
+ try {
305
+ const adbAvailable = await isAdbAvailable();
306
+ if (!adbAvailable) {
307
+ return {
308
+ success: false,
309
+ error: "ADB is not installed or not in PATH. Install Android SDK Platform Tools."
310
+ };
311
+ }
312
+ const deviceArg = buildDeviceArg(deviceId);
313
+ const device = deviceId || (await getDefaultAndroidDevice());
314
+ if (!device) {
315
+ return {
316
+ success: false,
317
+ error: "No Android device connected. Connect a device or start an emulator."
318
+ };
319
+ }
320
+ const { stdout } = await execAsync(`adb ${deviceArg} shell pm list packages`, {
321
+ timeout: ADB_TIMEOUT
322
+ });
323
+ let packages = stdout
324
+ .trim()
325
+ .split("\n")
326
+ .map((line) => line.replace("package:", "").trim())
327
+ .filter((pkg) => pkg.length > 0);
328
+ if (filter) {
329
+ const filterLower = filter.toLowerCase();
330
+ packages = packages.filter((pkg) => pkg.toLowerCase().includes(filterLower));
331
+ }
332
+ if (packages.length === 0) {
333
+ return {
334
+ success: true,
335
+ result: filter ? `No packages found matching "${filter}"` : "No packages found"
336
+ };
337
+ }
338
+ return {
339
+ success: true,
340
+ result: `Installed packages${filter ? ` matching "${filter}"` : ""}:\n${packages.join("\n")}`
341
+ };
342
+ }
343
+ catch (error) {
344
+ return {
345
+ success: false,
346
+ error: `Failed to list packages: ${error instanceof Error ? error.message : String(error)}`
347
+ };
348
+ }
349
+ }
350
+ // ============================================================================
351
+ // UI Input Functions (Phase 2)
352
+ // ============================================================================
353
+ /**
354
+ * Common key event codes for Android
355
+ */
356
+ export const ANDROID_KEY_EVENTS = {
357
+ HOME: 3,
358
+ BACK: 4,
359
+ CALL: 5,
360
+ END_CALL: 6,
361
+ VOLUME_UP: 24,
362
+ VOLUME_DOWN: 25,
363
+ POWER: 26,
364
+ CAMERA: 27,
365
+ CLEAR: 28,
366
+ TAB: 61,
367
+ ENTER: 66,
368
+ DEL: 67,
369
+ MENU: 82,
370
+ SEARCH: 84,
371
+ MEDIA_PLAY_PAUSE: 85,
372
+ MEDIA_STOP: 86,
373
+ MEDIA_NEXT: 87,
374
+ MEDIA_PREVIOUS: 88,
375
+ MOVE_HOME: 122,
376
+ MOVE_END: 123,
377
+ APP_SWITCH: 187,
378
+ ESCAPE: 111
379
+ };
380
+ /**
381
+ * Tap at coordinates on an Android device
382
+ */
383
+ export async function androidTap(x, y, deviceId) {
384
+ try {
385
+ const adbAvailable = await isAdbAvailable();
386
+ if (!adbAvailable) {
387
+ return {
388
+ success: false,
389
+ error: "ADB is not installed or not in PATH. Install Android SDK Platform Tools."
390
+ };
391
+ }
392
+ const deviceArg = buildDeviceArg(deviceId);
393
+ const device = deviceId || (await getDefaultAndroidDevice());
394
+ if (!device) {
395
+ return {
396
+ success: false,
397
+ error: "No Android device connected. Connect a device or start an emulator."
398
+ };
399
+ }
400
+ await execAsync(`adb ${deviceArg} shell input tap ${Math.round(x)} ${Math.round(y)}`, {
401
+ timeout: ADB_TIMEOUT
402
+ });
403
+ return {
404
+ success: true,
405
+ result: `Tapped at (${Math.round(x)}, ${Math.round(y)})`
406
+ };
407
+ }
408
+ catch (error) {
409
+ return {
410
+ success: false,
411
+ error: `Failed to tap: ${error instanceof Error ? error.message : String(error)}`
412
+ };
413
+ }
414
+ }
415
+ /**
416
+ * Long press at coordinates on an Android device
417
+ */
418
+ export async function androidLongPress(x, y, durationMs = 1000, deviceId) {
419
+ try {
420
+ const adbAvailable = await isAdbAvailable();
421
+ if (!adbAvailable) {
422
+ return {
423
+ success: false,
424
+ error: "ADB is not installed or not in PATH. Install Android SDK Platform Tools."
425
+ };
426
+ }
427
+ const deviceArg = buildDeviceArg(deviceId);
428
+ const device = deviceId || (await getDefaultAndroidDevice());
429
+ if (!device) {
430
+ return {
431
+ success: false,
432
+ error: "No Android device connected. Connect a device or start an emulator."
433
+ };
434
+ }
435
+ // Long press is implemented as a swipe from the same point to the same point
436
+ const xRounded = Math.round(x);
437
+ const yRounded = Math.round(y);
438
+ await execAsync(`adb ${deviceArg} shell input swipe ${xRounded} ${yRounded} ${xRounded} ${yRounded} ${durationMs}`, { timeout: ADB_TIMEOUT + durationMs });
439
+ return {
440
+ success: true,
441
+ result: `Long pressed at (${xRounded}, ${yRounded}) for ${durationMs}ms`
442
+ };
443
+ }
444
+ catch (error) {
445
+ return {
446
+ success: false,
447
+ error: `Failed to long press: ${error instanceof Error ? error.message : String(error)}`
448
+ };
449
+ }
450
+ }
451
+ /**
452
+ * Swipe on an Android device
453
+ */
454
+ export async function androidSwipe(startX, startY, endX, endY, durationMs = 300, deviceId) {
455
+ try {
456
+ const adbAvailable = await isAdbAvailable();
457
+ if (!adbAvailable) {
458
+ return {
459
+ success: false,
460
+ error: "ADB is not installed or not in PATH. Install Android SDK Platform Tools."
461
+ };
462
+ }
463
+ const deviceArg = buildDeviceArg(deviceId);
464
+ const device = deviceId || (await getDefaultAndroidDevice());
465
+ if (!device) {
466
+ return {
467
+ success: false,
468
+ error: "No Android device connected. Connect a device or start an emulator."
469
+ };
470
+ }
471
+ const x1 = Math.round(startX);
472
+ const y1 = Math.round(startY);
473
+ const x2 = Math.round(endX);
474
+ const y2 = Math.round(endY);
475
+ await execAsync(`adb ${deviceArg} shell input swipe ${x1} ${y1} ${x2} ${y2} ${durationMs}`, { timeout: ADB_TIMEOUT + durationMs });
476
+ return {
477
+ success: true,
478
+ result: `Swiped from (${x1}, ${y1}) to (${x2}, ${y2}) in ${durationMs}ms`
479
+ };
480
+ }
481
+ catch (error) {
482
+ return {
483
+ success: false,
484
+ error: `Failed to swipe: ${error instanceof Error ? error.message : String(error)}`
485
+ };
486
+ }
487
+ }
488
+ /**
489
+ * Input text on an Android device
490
+ *
491
+ * ADB input text has limitations with special characters.
492
+ * This function handles escaping properly for URLs, emails, and special strings.
493
+ */
494
+ export async function androidInputText(text, deviceId) {
495
+ try {
496
+ const adbAvailable = await isAdbAvailable();
497
+ if (!adbAvailable) {
498
+ return {
499
+ success: false,
500
+ error: "ADB is not installed or not in PATH. Install Android SDK Platform Tools."
501
+ };
502
+ }
503
+ const deviceArg = buildDeviceArg(deviceId);
504
+ const device = deviceId || (await getDefaultAndroidDevice());
505
+ if (!device) {
506
+ return {
507
+ success: false,
508
+ error: "No Android device connected. Connect a device or start an emulator."
509
+ };
510
+ }
511
+ // For complex strings with special characters, type character by character
512
+ // using key events for reliability
513
+ const hasComplexChars = /[/:?=&#@%+]/.test(text);
514
+ if (hasComplexChars) {
515
+ // Use character-by-character input for strings with special chars
516
+ // This is slower but more reliable for URLs, emails, etc.
517
+ for (const char of text) {
518
+ let keyCmd;
519
+ // Map special characters to their escaped form or use direct input
520
+ switch (char) {
521
+ case " ":
522
+ keyCmd = `adb ${deviceArg} shell input text "%s"`;
523
+ break;
524
+ case "'":
525
+ // Single quote needs special handling
526
+ keyCmd = `adb ${deviceArg} shell input text "\\'"`;
527
+ break;
528
+ case '"':
529
+ keyCmd = `adb ${deviceArg} shell input text '\\"'`;
530
+ break;
531
+ case "\\":
532
+ keyCmd = `adb ${deviceArg} shell input text "\\\\"`;
533
+ break;
534
+ case "&":
535
+ keyCmd = `adb ${deviceArg} shell input text "\\&"`;
536
+ break;
537
+ case "|":
538
+ keyCmd = `adb ${deviceArg} shell input text "\\|"`;
539
+ break;
540
+ case ";":
541
+ keyCmd = `adb ${deviceArg} shell input text "\\;"`;
542
+ break;
543
+ case "<":
544
+ keyCmd = `adb ${deviceArg} shell input text "\\<"`;
545
+ break;
546
+ case ">":
547
+ keyCmd = `adb ${deviceArg} shell input text "\\>"`;
548
+ break;
549
+ case "(":
550
+ keyCmd = `adb ${deviceArg} shell input text "\\("`;
551
+ break;
552
+ case ")":
553
+ keyCmd = `adb ${deviceArg} shell input text "\\)"`;
554
+ break;
555
+ case "$":
556
+ keyCmd = `adb ${deviceArg} shell input text "\\$"`;
557
+ break;
558
+ case "`":
559
+ keyCmd = `adb ${deviceArg} shell input text "\\\`"`;
560
+ break;
561
+ default:
562
+ // For most characters, wrap in single quotes to prevent shell interpretation
563
+ // Single quotes preserve literal meaning of all characters except single quote itself
564
+ keyCmd = `adb ${deviceArg} shell input text '${char}'`;
565
+ }
566
+ await execAsync(keyCmd, { timeout: 5000 });
567
+ }
568
+ return {
569
+ success: true,
570
+ result: `Typed: "${text}"`
571
+ };
572
+ }
573
+ // For simple alphanumeric strings, use the faster bulk input
574
+ // Escape basic special characters
575
+ const escapedText = text
576
+ .replace(/\\/g, "\\\\")
577
+ .replace(/"/g, '\\"')
578
+ .replace(/'/g, "\\'")
579
+ .replace(/`/g, "\\`")
580
+ .replace(/\$/g, "\\$")
581
+ .replace(/ /g, "%s");
582
+ await execAsync(`adb ${deviceArg} shell input text "${escapedText}"`, {
583
+ timeout: ADB_TIMEOUT
584
+ });
585
+ return {
586
+ success: true,
587
+ result: `Typed: "${text}"`
588
+ };
589
+ }
590
+ catch (error) {
591
+ return {
592
+ success: false,
593
+ error: `Failed to input text: ${error instanceof Error ? error.message : String(error)}`
594
+ };
595
+ }
596
+ }
597
+ /**
598
+ * Send a key event to an Android device
599
+ */
600
+ export async function androidKeyEvent(keyCode, deviceId) {
601
+ try {
602
+ const adbAvailable = await isAdbAvailable();
603
+ if (!adbAvailable) {
604
+ return {
605
+ success: false,
606
+ error: "ADB is not installed or not in PATH. Install Android SDK Platform Tools."
607
+ };
608
+ }
609
+ const deviceArg = buildDeviceArg(deviceId);
610
+ const device = deviceId || (await getDefaultAndroidDevice());
611
+ if (!device) {
612
+ return {
613
+ success: false,
614
+ error: "No Android device connected. Connect a device or start an emulator."
615
+ };
616
+ }
617
+ // Resolve key code from name if needed
618
+ const resolvedKeyCode = typeof keyCode === "string" ? ANDROID_KEY_EVENTS[keyCode] : keyCode;
619
+ if (resolvedKeyCode === undefined) {
620
+ return {
621
+ success: false,
622
+ error: `Invalid key code: ${keyCode}`
623
+ };
624
+ }
625
+ await execAsync(`adb ${deviceArg} shell input keyevent ${resolvedKeyCode}`, {
626
+ timeout: ADB_TIMEOUT
627
+ });
628
+ // Get key name for display
629
+ const keyName = typeof keyCode === "string"
630
+ ? keyCode
631
+ : Object.entries(ANDROID_KEY_EVENTS).find(([_, v]) => v === keyCode)?.[0] ||
632
+ `keycode ${keyCode}`;
633
+ return {
634
+ success: true,
635
+ result: `Sent key event: ${keyName}`
636
+ };
637
+ }
638
+ catch (error) {
639
+ return {
640
+ success: false,
641
+ error: `Failed to send key event: ${error instanceof Error ? error.message : String(error)}`
642
+ };
643
+ }
644
+ }
645
+ /**
646
+ * Parse bounds string like "[0,0][1080,1920]" to AndroidUIElement bounds
647
+ */
648
+ function parseBoundsForUIElement(boundsStr) {
649
+ const match = boundsStr.match(/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/);
650
+ if (!match)
651
+ return null;
652
+ const left = parseInt(match[1], 10);
653
+ const top = parseInt(match[2], 10);
654
+ const right = parseInt(match[3], 10);
655
+ const bottom = parseInt(match[4], 10);
656
+ return {
657
+ left,
658
+ top,
659
+ right,
660
+ bottom,
661
+ width: right - left,
662
+ height: bottom - top
663
+ };
664
+ }
665
+ /**
666
+ * Parse uiautomator XML dump into element array
667
+ */
668
+ function parseUIAutomatorXML(xml) {
669
+ const elements = [];
670
+ // Match all node elements with their attributes
671
+ const nodeRegex = /<node\s+([^>]+)\/?>|<node\s+([^>]+)>/g;
672
+ let match;
673
+ while ((match = nodeRegex.exec(xml)) !== null) {
674
+ const attrStr = match[1] || match[2];
675
+ if (!attrStr)
676
+ continue;
677
+ // Extract attributes
678
+ const getAttr = (name) => {
679
+ const attrMatch = attrStr.match(new RegExp(`${name}="([^"]*)"`));
680
+ return attrMatch ? attrMatch[1] : "";
681
+ };
682
+ const boundsStr = getAttr("bounds");
683
+ const bounds = parseBoundsForUIElement(boundsStr);
684
+ if (!bounds)
685
+ continue;
686
+ const element = {
687
+ text: getAttr("text"),
688
+ contentDesc: getAttr("content-desc"),
689
+ resourceId: getAttr("resource-id"),
690
+ className: getAttr("class"),
691
+ bounds,
692
+ center: {
693
+ x: Math.round((bounds.left + bounds.right) / 2),
694
+ y: Math.round((bounds.top + bounds.bottom) / 2)
695
+ },
696
+ clickable: getAttr("clickable") === "true",
697
+ enabled: getAttr("enabled") === "true",
698
+ focused: getAttr("focused") === "true",
699
+ scrollable: getAttr("scrollable") === "true",
700
+ selected: getAttr("selected") === "true"
701
+ };
702
+ elements.push(element);
703
+ }
704
+ return elements;
705
+ }
706
+ /**
707
+ * Match element against find options
708
+ */
709
+ function matchesElement(element, options) {
710
+ if (options.text !== undefined) {
711
+ if (element.text !== options.text)
712
+ return false;
713
+ }
714
+ if (options.textContains !== undefined) {
715
+ if (!element.text.toLowerCase().includes(options.textContains.toLowerCase()))
716
+ return false;
717
+ }
718
+ if (options.contentDesc !== undefined) {
719
+ if (element.contentDesc !== options.contentDesc)
720
+ return false;
721
+ }
722
+ if (options.contentDescContains !== undefined) {
723
+ if (!element.contentDesc.toLowerCase().includes(options.contentDescContains.toLowerCase()))
724
+ return false;
725
+ }
726
+ if (options.resourceId !== undefined) {
727
+ // Support both full "com.app:id/button" and short "button" forms
728
+ const shortId = element.resourceId.split("/").pop() || "";
729
+ if (element.resourceId !== options.resourceId && shortId !== options.resourceId)
730
+ return false;
731
+ }
732
+ return true;
733
+ }
734
+ /**
735
+ * Get UI accessibility tree from Android device using uiautomator
736
+ */
737
+ export async function androidGetUITree(deviceId) {
738
+ try {
739
+ const adbAvailable = await isAdbAvailable();
740
+ if (!adbAvailable) {
741
+ return {
742
+ success: false,
743
+ error: "ADB is not installed or not in PATH. Install Android SDK Platform Tools."
744
+ };
745
+ }
746
+ const deviceArg = buildDeviceArg(deviceId);
747
+ const device = deviceId || (await getDefaultAndroidDevice());
748
+ if (!device) {
749
+ return {
750
+ success: false,
751
+ error: "No Android device connected. Connect a device or start an emulator."
752
+ };
753
+ }
754
+ // Dump UI hierarchy to device
755
+ const remotePath = "/sdcard/ui_dump.xml";
756
+ await execAsync(`adb ${deviceArg} shell uiautomator dump ${remotePath}`, {
757
+ timeout: ADB_TIMEOUT
758
+ });
759
+ // Read the XML content
760
+ const { stdout } = await execAsync(`adb ${deviceArg} shell cat ${remotePath}`, {
761
+ timeout: ADB_TIMEOUT
762
+ });
763
+ // Clean up remote file
764
+ await execAsync(`adb ${deviceArg} shell rm ${remotePath}`, {
765
+ timeout: ADB_TIMEOUT
766
+ }).catch(() => { });
767
+ const elements = parseUIAutomatorXML(stdout);
768
+ return {
769
+ success: true,
770
+ elements,
771
+ rawXml: stdout
772
+ };
773
+ }
774
+ catch (error) {
775
+ return {
776
+ success: false,
777
+ error: `Failed to get UI tree: ${error instanceof Error ? error.message : String(error)}`
778
+ };
779
+ }
780
+ }
781
+ /**
782
+ * Find element(s) in the UI tree matching the given criteria
783
+ */
784
+ export async function androidFindElement(options, deviceId) {
785
+ try {
786
+ // Validate that at least one search criteria is provided
787
+ if (!options.text && !options.textContains && !options.contentDesc &&
788
+ !options.contentDescContains && !options.resourceId) {
789
+ return {
790
+ success: false,
791
+ found: false,
792
+ error: "At least one search criteria (text, textContains, contentDesc, contentDescContains, or resourceId) must be provided"
793
+ };
794
+ }
795
+ const treeResult = await androidGetUITree(deviceId);
796
+ if (!treeResult.success || !treeResult.elements) {
797
+ return {
798
+ success: false,
799
+ found: false,
800
+ error: treeResult.error
801
+ };
802
+ }
803
+ // Find matching elements
804
+ const matches = treeResult.elements.filter(el => matchesElement(el, options));
805
+ if (matches.length === 0) {
806
+ return {
807
+ success: true,
808
+ found: false,
809
+ matchCount: 0
810
+ };
811
+ }
812
+ // Select the element at the specified index (default 0)
813
+ const index = options.index ?? 0;
814
+ const selectedElement = matches[index];
815
+ if (!selectedElement) {
816
+ return {
817
+ success: true,
818
+ found: false,
819
+ matchCount: matches.length,
820
+ error: `Index ${index} out of bounds. Found ${matches.length} matching element(s).`
821
+ };
822
+ }
823
+ return {
824
+ success: true,
825
+ found: true,
826
+ element: selectedElement,
827
+ allMatches: matches,
828
+ matchCount: matches.length
829
+ };
830
+ }
831
+ catch (error) {
832
+ return {
833
+ success: false,
834
+ found: false,
835
+ error: `Failed to find element: ${error instanceof Error ? error.message : String(error)}`
836
+ };
837
+ }
838
+ }
839
+ /**
840
+ * Wait for element to appear on screen with polling
841
+ */
842
+ export async function androidWaitForElement(options, deviceId) {
843
+ const timeoutMs = options.timeoutMs ?? 10000;
844
+ const pollIntervalMs = options.pollIntervalMs ?? 500;
845
+ const startTime = Date.now();
846
+ // Validate that at least one search criteria is provided
847
+ if (!options.text && !options.textContains && !options.contentDesc &&
848
+ !options.contentDescContains && !options.resourceId) {
849
+ return {
850
+ success: false,
851
+ found: false,
852
+ timedOut: false,
853
+ error: "At least one search criteria (text, textContains, contentDesc, contentDescContains, or resourceId) must be provided"
854
+ };
855
+ }
856
+ while (Date.now() - startTime < timeoutMs) {
857
+ const result = await androidFindElement(options, deviceId);
858
+ if (result.found && result.element) {
859
+ return {
860
+ ...result,
861
+ elapsedMs: Date.now() - startTime,
862
+ timedOut: false
863
+ };
864
+ }
865
+ // If there was an error (not just "not found"), return it
866
+ if (!result.success) {
867
+ return {
868
+ ...result,
869
+ elapsedMs: Date.now() - startTime,
870
+ timedOut: false
871
+ };
872
+ }
873
+ // Wait before next poll
874
+ await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
875
+ }
876
+ return {
877
+ success: true,
878
+ found: false,
879
+ elapsedMs: Date.now() - startTime,
880
+ timedOut: true,
881
+ error: `Timed out after ${timeoutMs}ms waiting for element`
882
+ };
883
+ }
884
+ /**
885
+ * Get device screen size
886
+ */
887
+ export async function androidGetScreenSize(deviceId) {
888
+ try {
889
+ const adbAvailable = await isAdbAvailable();
890
+ if (!adbAvailable) {
891
+ return {
892
+ success: false,
893
+ error: "ADB is not installed or not in PATH. Install Android SDK Platform Tools."
894
+ };
895
+ }
896
+ const deviceArg = buildDeviceArg(deviceId);
897
+ const device = deviceId || (await getDefaultAndroidDevice());
898
+ if (!device) {
899
+ return {
900
+ success: false,
901
+ error: "No Android device connected. Connect a device or start an emulator."
902
+ };
903
+ }
904
+ const { stdout } = await execAsync(`adb ${deviceArg} shell wm size`, {
905
+ timeout: ADB_TIMEOUT
906
+ });
907
+ // Parse output like "Physical size: 1080x1920"
908
+ const match = stdout.match(/(\d+)x(\d+)/);
909
+ if (match) {
910
+ return {
911
+ success: true,
912
+ width: parseInt(match[1], 10),
913
+ height: parseInt(match[2], 10)
914
+ };
915
+ }
916
+ return {
917
+ success: false,
918
+ error: `Could not parse screen size from: ${stdout.trim()}`
919
+ };
920
+ }
921
+ catch (error) {
922
+ return {
923
+ success: false,
924
+ error: `Failed to get screen size: ${error instanceof Error ? error.message : String(error)}`
925
+ };
926
+ }
927
+ }
928
+ /**
929
+ * Get device display density (dpi)
930
+ */
931
+ export async function androidGetDensity(deviceId) {
932
+ try {
933
+ const adbAvailable = await isAdbAvailable();
934
+ if (!adbAvailable) {
935
+ return {
936
+ success: false,
937
+ error: "ADB is not installed or not in PATH. Install Android SDK Platform Tools."
938
+ };
939
+ }
940
+ const deviceArg = buildDeviceArg(deviceId);
941
+ const device = deviceId || (await getDefaultAndroidDevice());
942
+ if (!device) {
943
+ return {
944
+ success: false,
945
+ error: "No Android device connected. Connect a device or start an emulator."
946
+ };
947
+ }
948
+ const { stdout } = await execAsync(`adb ${deviceArg} shell wm density`, {
949
+ timeout: ADB_TIMEOUT
950
+ });
951
+ // Parse output like "Physical density: 440" or "Override density: 440"
952
+ const match = stdout.match(/density:\s*(\d+)/i);
953
+ if (match) {
954
+ return {
955
+ success: true,
956
+ density: parseInt(match[1], 10)
957
+ };
958
+ }
959
+ return {
960
+ success: false,
961
+ error: `Could not parse density from: ${stdout.trim()}`
962
+ };
963
+ }
964
+ catch (error) {
965
+ return {
966
+ success: false,
967
+ error: `Failed to get density: ${error instanceof Error ? error.message : String(error)}`
968
+ };
969
+ }
970
+ }
971
+ /**
972
+ * Get status bar height in pixels
973
+ * Android status bar is typically 24dp, but can vary by device/OS version
974
+ */
975
+ export async function androidGetStatusBarHeight(deviceId) {
976
+ try {
977
+ // Get density first
978
+ const densityResult = await androidGetDensity(deviceId);
979
+ if (!densityResult.success || !densityResult.density) {
980
+ // Fallback to common estimate
981
+ return {
982
+ success: true,
983
+ heightPixels: 63, // Common for 420dpi devices (24dp * 2.625)
984
+ heightDp: 24
985
+ };
986
+ }
987
+ const density = densityResult.density;
988
+ const densityScale = density / 160; // Android baseline is 160dpi
989
+ // Try to get actual status bar height from resources
990
+ const deviceArg = buildDeviceArg(deviceId);
991
+ try {
992
+ const { stdout } = await execAsync(`adb ${deviceArg} shell "dumpsys window | grep -E 'statusBars|mStatusBarLayer|InsetsSource.*statusBars'"`, { timeout: ADB_TIMEOUT });
993
+ // Try to parse status bar height from dumpsys output
994
+ // Look for patterns like "statusBars frame=[0,0][1080,63]"
995
+ const frameMatch = stdout.match(/statusBars.*frame=\[[\d,]+\]\[(\d+),(\d+)\]/);
996
+ if (frameMatch) {
997
+ const heightPixels = parseInt(frameMatch[2], 10);
998
+ return {
999
+ success: true,
1000
+ heightPixels,
1001
+ heightDp: Math.round(heightPixels / densityScale)
1002
+ };
1003
+ }
1004
+ }
1005
+ catch {
1006
+ // Fallback to standard calculation
1007
+ }
1008
+ // Standard status bar height is 24dp on most devices
1009
+ const statusBarDp = 24;
1010
+ const statusBarPixels = Math.round(statusBarDp * densityScale);
1011
+ return {
1012
+ success: true,
1013
+ heightPixels: statusBarPixels,
1014
+ heightDp: statusBarDp
1015
+ };
1016
+ }
1017
+ catch (error) {
1018
+ return {
1019
+ success: false,
1020
+ error: `Failed to get status bar height: ${error instanceof Error ? error.message : String(error)}`
1021
+ };
1022
+ }
1023
+ }
1024
+ /**
1025
+ * Simplify Android class name for display
1026
+ * android.widget.Button -> Button
1027
+ * android.widget.TextView -> TextView
1028
+ */
1029
+ function simplifyClassName(className) {
1030
+ if (!className)
1031
+ return "Unknown";
1032
+ const parts = className.split(".");
1033
+ return parts[parts.length - 1];
1034
+ }
1035
+ /**
1036
+ * Parse bounds string "[left,top][right,bottom]" to object
1037
+ */
1038
+ function parseBounds(boundsStr) {
1039
+ const match = boundsStr?.match(/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/);
1040
+ if (!match)
1041
+ return null;
1042
+ return {
1043
+ left: parseInt(match[1], 10),
1044
+ top: parseInt(match[2], 10),
1045
+ right: parseInt(match[3], 10),
1046
+ bottom: parseInt(match[4], 10)
1047
+ };
1048
+ }
1049
+ /**
1050
+ * Parse a single node from uiautomator XML
1051
+ */
1052
+ function parseUiNode(node) {
1053
+ const attrs = node["@_bounds"]
1054
+ ? node
1055
+ : node.node
1056
+ ? (Array.isArray(node.node) ? node.node[0] : node.node)
1057
+ : null;
1058
+ if (!attrs)
1059
+ return null;
1060
+ const boundsStr = attrs["@_bounds"];
1061
+ const bounds = parseBounds(boundsStr);
1062
+ if (!bounds)
1063
+ return null;
1064
+ const width = bounds.right - bounds.left;
1065
+ const height = bounds.bottom - bounds.top;
1066
+ const centerX = Math.round(bounds.left + width / 2);
1067
+ const centerY = Math.round(bounds.top + height / 2);
1068
+ const element = {
1069
+ class: simplifyClassName(attrs["@_class"] || ""),
1070
+ bounds,
1071
+ frame: {
1072
+ x: bounds.left,
1073
+ y: bounds.top,
1074
+ width,
1075
+ height
1076
+ },
1077
+ tap: {
1078
+ x: centerX,
1079
+ y: centerY
1080
+ },
1081
+ children: []
1082
+ };
1083
+ // Add optional attributes
1084
+ if (attrs["@_text"])
1085
+ element.text = attrs["@_text"];
1086
+ if (attrs["@_content-desc"])
1087
+ element.contentDesc = attrs["@_content-desc"];
1088
+ if (attrs["@_resource-id"])
1089
+ element.resourceId = attrs["@_resource-id"];
1090
+ if (attrs["@_checkable"] === "true")
1091
+ element.checkable = true;
1092
+ if (attrs["@_checked"] === "true")
1093
+ element.checked = true;
1094
+ if (attrs["@_clickable"] === "true")
1095
+ element.clickable = true;
1096
+ if (attrs["@_enabled"] === "true")
1097
+ element.enabled = true;
1098
+ if (attrs["@_focusable"] === "true")
1099
+ element.focusable = true;
1100
+ if (attrs["@_focused"] === "true")
1101
+ element.focused = true;
1102
+ if (attrs["@_scrollable"] === "true")
1103
+ element.scrollable = true;
1104
+ if (attrs["@_selected"] === "true")
1105
+ element.selected = true;
1106
+ return element;
1107
+ }
1108
+ /**
1109
+ * Recursively parse UI hierarchy from XML node
1110
+ */
1111
+ function parseHierarchy(node) {
1112
+ const results = [];
1113
+ // Handle the node itself
1114
+ if (node["@_bounds"]) {
1115
+ const element = parseUiNode(node);
1116
+ if (element) {
1117
+ // Parse children
1118
+ if (node.node) {
1119
+ const children = Array.isArray(node.node) ? node.node : [node.node];
1120
+ for (const child of children) {
1121
+ element.children.push(...parseHierarchy(child));
1122
+ }
1123
+ }
1124
+ results.push(element);
1125
+ }
1126
+ }
1127
+ else if (node.node) {
1128
+ // This is a container without bounds (like hierarchy root)
1129
+ const children = Array.isArray(node.node) ? node.node : [node.node];
1130
+ for (const child of children) {
1131
+ results.push(...parseHierarchy(child));
1132
+ }
1133
+ }
1134
+ return results;
1135
+ }
1136
+ /**
1137
+ * Format accessibility tree for display (similar to iOS format)
1138
+ */
1139
+ function formatAndroidAccessibilityTree(elements, indent = 0) {
1140
+ const lines = [];
1141
+ const prefix = " ".repeat(indent);
1142
+ for (const element of elements) {
1143
+ const parts = [];
1144
+ // [ClassName] "text" or "content-desc"
1145
+ parts.push(`[${element.class}]`);
1146
+ // Add label (text or content-desc)
1147
+ const label = element.text || element.contentDesc;
1148
+ if (label) {
1149
+ parts.push(`"${label}"`);
1150
+ }
1151
+ // Add frame and tap coordinates
1152
+ const f = element.frame;
1153
+ parts.push(`frame=(${f.x}, ${f.y}, ${f.width}x${f.height}) tap=(${element.tap.x}, ${element.tap.y})`);
1154
+ lines.push(`${prefix}${parts.join(" ")}`);
1155
+ // Recurse into children
1156
+ if (element.children.length > 0) {
1157
+ lines.push(formatAndroidAccessibilityTree(element.children, indent + 1));
1158
+ }
1159
+ }
1160
+ return lines.join("\n");
1161
+ }
1162
+ /**
1163
+ * Flatten element tree to array for searching
1164
+ */
1165
+ function flattenElements(elements) {
1166
+ const result = [];
1167
+ for (const element of elements) {
1168
+ result.push(element);
1169
+ if (element.children.length > 0) {
1170
+ result.push(...flattenElements(element.children));
1171
+ }
1172
+ }
1173
+ return result;
1174
+ }
1175
+ /**
1176
+ * Get the UI hierarchy from the connected Android device using uiautomator dump
1177
+ */
1178
+ export async function androidDescribeAll(deviceId) {
1179
+ try {
1180
+ const adbAvailable = await isAdbAvailable();
1181
+ if (!adbAvailable) {
1182
+ return {
1183
+ success: false,
1184
+ error: "ADB is not installed or not in PATH. Install Android SDK Platform Tools."
1185
+ };
1186
+ }
1187
+ const deviceArg = buildDeviceArg(deviceId);
1188
+ const device = deviceId || (await getDefaultAndroidDevice());
1189
+ if (!device) {
1190
+ return {
1191
+ success: false,
1192
+ error: "No Android device connected. Connect a device or start an emulator."
1193
+ };
1194
+ }
1195
+ // Use file-based approach (most reliable across devices)
1196
+ // /dev/tty doesn't work on most emulators/devices
1197
+ const remotePath = "/sdcard/ui_dump.xml";
1198
+ await execAsync(`adb ${deviceArg} shell uiautomator dump ${remotePath}`, {
1199
+ timeout: ADB_TIMEOUT
1200
+ });
1201
+ const { stdout } = await execAsync(`adb ${deviceArg} shell cat ${remotePath}`, {
1202
+ timeout: ADB_TIMEOUT,
1203
+ maxBuffer: 10 * 1024 * 1024
1204
+ });
1205
+ const xmlContent = stdout.trim();
1206
+ // Clean up
1207
+ await execAsync(`adb ${deviceArg} shell rm ${remotePath}`, {
1208
+ timeout: 5000
1209
+ }).catch(() => { });
1210
+ if (!xmlContent || !xmlContent.includes("<hierarchy")) {
1211
+ return {
1212
+ success: false,
1213
+ error: "Failed to get UI hierarchy. Make sure the device screen is unlocked and the app is in foreground."
1214
+ };
1215
+ }
1216
+ // Parse XML
1217
+ const parser = new XMLParser({
1218
+ ignoreAttributes: false,
1219
+ attributeNamePrefix: "@_"
1220
+ });
1221
+ const parsed = parser.parse(xmlContent);
1222
+ if (!parsed.hierarchy) {
1223
+ return {
1224
+ success: false,
1225
+ error: "Invalid UI hierarchy XML structure"
1226
+ };
1227
+ }
1228
+ const elements = parseHierarchy(parsed.hierarchy);
1229
+ const formatted = formatAndroidAccessibilityTree(elements);
1230
+ return {
1231
+ success: true,
1232
+ elements,
1233
+ formatted
1234
+ };
1235
+ }
1236
+ catch (error) {
1237
+ return {
1238
+ success: false,
1239
+ error: `Failed to get UI hierarchy: ${error instanceof Error ? error.message : String(error)}`
1240
+ };
1241
+ }
1242
+ }
1243
+ /**
1244
+ * Get accessibility info for the UI element at specific coordinates
1245
+ */
1246
+ export async function androidDescribePoint(x, y, deviceId) {
1247
+ try {
1248
+ // First get the full hierarchy
1249
+ const result = await androidDescribeAll(deviceId);
1250
+ if (!result.success || !result.elements) {
1251
+ return result;
1252
+ }
1253
+ // Flatten and find elements containing the point
1254
+ const allElements = flattenElements(result.elements);
1255
+ // Find all elements whose bounds contain the point
1256
+ const matchingElements = allElements.filter((el) => {
1257
+ const b = el.bounds;
1258
+ return x >= b.left && x <= b.right && y >= b.top && y <= b.bottom;
1259
+ });
1260
+ if (matchingElements.length === 0) {
1261
+ return {
1262
+ success: true,
1263
+ formatted: `No element found at (${x}, ${y})`
1264
+ };
1265
+ }
1266
+ // Return the deepest (smallest) element that contains the point
1267
+ // Sort by area (smallest first) to get the most specific element
1268
+ matchingElements.sort((a, b) => {
1269
+ const areaA = a.frame.width * a.frame.height;
1270
+ const areaB = b.frame.width * b.frame.height;
1271
+ return areaA - areaB;
1272
+ });
1273
+ const element = matchingElements[0];
1274
+ // Format detailed output
1275
+ const lines = [];
1276
+ const label = element.text || element.contentDesc;
1277
+ lines.push(`[${element.class}]${label ? ` "${label}"` : ""} frame=(${element.frame.x}, ${element.frame.y}, ${element.frame.width}x${element.frame.height}) tap=(${element.tap.x}, ${element.tap.y})`);
1278
+ if (element.resourceId) {
1279
+ lines.push(` resource-id: ${element.resourceId}`);
1280
+ }
1281
+ if (element.contentDesc && element.text) {
1282
+ // Show content-desc separately if we showed text as label
1283
+ lines.push(` content-desc: ${element.contentDesc}`);
1284
+ }
1285
+ if (element.text && element.contentDesc) {
1286
+ // Show text separately if we showed content-desc as label
1287
+ lines.push(` text: ${element.text}`);
1288
+ }
1289
+ // Show state flags
1290
+ const flags = [];
1291
+ if (element.clickable)
1292
+ flags.push("clickable");
1293
+ if (element.enabled)
1294
+ flags.push("enabled");
1295
+ if (element.focusable)
1296
+ flags.push("focusable");
1297
+ if (element.focused)
1298
+ flags.push("focused");
1299
+ if (element.scrollable)
1300
+ flags.push("scrollable");
1301
+ if (element.selected)
1302
+ flags.push("selected");
1303
+ if (element.checked)
1304
+ flags.push("checked");
1305
+ if (flags.length > 0) {
1306
+ lines.push(` state: ${flags.join(", ")}`);
1307
+ }
1308
+ return {
1309
+ success: true,
1310
+ elements: [element],
1311
+ formatted: lines.join("\n")
1312
+ };
1313
+ }
1314
+ catch (error) {
1315
+ return {
1316
+ success: false,
1317
+ error: `Failed to describe point: ${error instanceof Error ? error.message : String(error)}`
1318
+ };
1319
+ }
1320
+ }
1321
+ /**
1322
+ * Tap an element by its text, content-description, or resource-id
1323
+ */
1324
+ export async function androidTapElement(options) {
1325
+ try {
1326
+ const { text, textContains, contentDesc, contentDescContains, resourceId, index = 0, deviceId } = options;
1327
+ // Validate that at least one search criterion is provided
1328
+ if (!text && !textContains && !contentDesc && !contentDescContains && !resourceId) {
1329
+ return {
1330
+ success: false,
1331
+ error: "At least one of text, textContains, contentDesc, contentDescContains, or resourceId must be provided"
1332
+ };
1333
+ }
1334
+ // Get the UI hierarchy
1335
+ const result = await androidDescribeAll(deviceId);
1336
+ if (!result.success || !result.elements) {
1337
+ return {
1338
+ success: false,
1339
+ error: result.error || "Failed to get UI hierarchy"
1340
+ };
1341
+ }
1342
+ // Flatten and search
1343
+ const allElements = flattenElements(result.elements);
1344
+ // Filter elements based on search criteria
1345
+ const matchingElements = allElements.filter((el) => {
1346
+ if (text && el.text !== text)
1347
+ return false;
1348
+ if (textContains && (!el.text || !el.text.toLowerCase().includes(textContains.toLowerCase())))
1349
+ return false;
1350
+ if (contentDesc && el.contentDesc !== contentDesc)
1351
+ return false;
1352
+ if (contentDescContains && (!el.contentDesc || !el.contentDesc.toLowerCase().includes(contentDescContains.toLowerCase())))
1353
+ return false;
1354
+ if (resourceId) {
1355
+ // Support both full resource-id and short form
1356
+ if (!el.resourceId)
1357
+ return false;
1358
+ if (el.resourceId !== resourceId && !el.resourceId.endsWith(`:id/${resourceId}`))
1359
+ return false;
1360
+ }
1361
+ return true;
1362
+ });
1363
+ if (matchingElements.length === 0) {
1364
+ const criteria = [];
1365
+ if (text)
1366
+ criteria.push(`text="${text}"`);
1367
+ if (textContains)
1368
+ criteria.push(`textContains="${textContains}"`);
1369
+ if (contentDesc)
1370
+ criteria.push(`contentDesc="${contentDesc}"`);
1371
+ if (contentDescContains)
1372
+ criteria.push(`contentDescContains="${contentDescContains}"`);
1373
+ if (resourceId)
1374
+ criteria.push(`resourceId="${resourceId}"`);
1375
+ return {
1376
+ success: false,
1377
+ error: `Element not found: ${criteria.join(", ")}`
1378
+ };
1379
+ }
1380
+ if (index >= matchingElements.length) {
1381
+ return {
1382
+ success: false,
1383
+ error: `Index ${index} out of range. Found ${matchingElements.length} matching element(s).`
1384
+ };
1385
+ }
1386
+ const element = matchingElements[index];
1387
+ const label = element.text || element.contentDesc || element.resourceId || element.class;
1388
+ // Log if multiple matches
1389
+ let resultMessage;
1390
+ if (matchingElements.length > 1) {
1391
+ resultMessage = `Found ${matchingElements.length} elements, tapping "${label}" (index ${index}) at (${element.tap.x}, ${element.tap.y})`;
1392
+ }
1393
+ else {
1394
+ resultMessage = `Tapped "${label}" at (${element.tap.x}, ${element.tap.y})`;
1395
+ }
1396
+ // Perform the tap
1397
+ const tapResult = await androidTap(element.tap.x, element.tap.y, deviceId);
1398
+ if (!tapResult.success) {
1399
+ return tapResult;
1400
+ }
1401
+ return {
1402
+ success: true,
1403
+ result: resultMessage
1404
+ };
1405
+ }
1406
+ catch (error) {
1407
+ return {
1408
+ success: false,
1409
+ error: `Failed to tap element: ${error instanceof Error ? error.message : String(error)}`
1410
+ };
1411
+ }
1412
+ }
1413
+ //# sourceMappingURL=android.js.map