@rvaim/deveco-mobile-mcp 0.1.0-rvaim.1

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.
package/lib/android.js ADDED
@@ -0,0 +1,596 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.AndroidDeviceManager = exports.AndroidRobot = void 0;
40
+ const node_path_1 = __importDefault(require("node:path"));
41
+ const node_child_process_1 = require("node:child_process");
42
+ const node_fs_1 = require("node:fs");
43
+ const xml = __importStar(require("fast-xml-parser"));
44
+ const robot_1 = require("./robot");
45
+ const utils_1 = require("./utils");
46
+ const getAdbPath = () => {
47
+ const exeName = process.env.platform === "win32" ? "adb.exe" : "adb";
48
+ if (process.env.ANDROID_HOME) {
49
+ return node_path_1.default.join(process.env.ANDROID_HOME, "platform-tools", exeName);
50
+ }
51
+ if (process.platform === "win32" && process.env.LOCALAPPDATA) {
52
+ const windowsAdbPath = node_path_1.default.join(process.env.LOCALAPPDATA, "Android", "Sdk", "platform-tools", "adb.exe");
53
+ if ((0, node_fs_1.existsSync)(windowsAdbPath)) {
54
+ return windowsAdbPath;
55
+ }
56
+ }
57
+ if (process.platform === "darwin" && process.env.HOME) {
58
+ const defaultAndroidSdk = node_path_1.default.join(process.env.HOME, "Library", "Android", "sdk", "platform-tools", "adb");
59
+ if ((0, node_fs_1.existsSync)(defaultAndroidSdk)) {
60
+ return defaultAndroidSdk;
61
+ }
62
+ }
63
+ // fallthrough, hope for the best
64
+ return exeName;
65
+ };
66
+ const BUTTON_MAP = {
67
+ "BACK": "KEYCODE_BACK",
68
+ "HOME": "KEYCODE_HOME",
69
+ "VOLUME_UP": "KEYCODE_VOLUME_UP",
70
+ "VOLUME_DOWN": "KEYCODE_VOLUME_DOWN",
71
+ "ENTER": "KEYCODE_ENTER",
72
+ "DEL": "KEYCODE_DEL",
73
+ "DPAD_CENTER": "KEYCODE_DPAD_CENTER",
74
+ "DPAD_UP": "KEYCODE_DPAD_UP",
75
+ "DPAD_DOWN": "KEYCODE_DPAD_DOWN",
76
+ "DPAD_LEFT": "KEYCODE_DPAD_LEFT",
77
+ "DPAD_RIGHT": "KEYCODE_DPAD_RIGHT",
78
+ };
79
+ const TIMEOUT = 30000;
80
+ const MAX_BUFFER_SIZE = 1024 * 1024 * 8;
81
+ class AndroidRobot {
82
+ deviceId;
83
+ constructor(deviceId) {
84
+ this.deviceId = deviceId;
85
+ }
86
+ adb(...args) {
87
+ return (0, node_child_process_1.execFileSync)(getAdbPath(), ["-s", this.deviceId, ...args], {
88
+ maxBuffer: MAX_BUFFER_SIZE,
89
+ timeout: TIMEOUT,
90
+ });
91
+ }
92
+ silentAdb(...args) {
93
+ return (0, node_child_process_1.execFileSync)(getAdbPath(), ["-s", this.deviceId, ...args], {
94
+ maxBuffer: MAX_BUFFER_SIZE,
95
+ timeout: TIMEOUT,
96
+ stdio: ["pipe", "pipe", "pipe"],
97
+ });
98
+ }
99
+ getSystemFeatures() {
100
+ return this.adb("shell", "pm", "list", "features")
101
+ .toString()
102
+ .split("\n")
103
+ .map(line => line.trim())
104
+ .filter(line => line.startsWith("feature:"))
105
+ .map(line => line.substring("feature:".length));
106
+ }
107
+ async getScreenSize() {
108
+ const screenSize = this.adb("shell", "wm", "size")
109
+ .toString()
110
+ .split(" ")
111
+ .pop();
112
+ if (!screenSize) {
113
+ throw new Error("Failed to get screen size");
114
+ }
115
+ const scale = 1;
116
+ const [width, height] = screenSize.split("x").map(Number);
117
+ return { width, height, scale };
118
+ }
119
+ async listApps() {
120
+ // only apps that have a launcher activity are returned
121
+ return this.adb("shell", "cmd", "package", "query-activities", "-a", "android.intent.action.MAIN", "-c", "android.intent.category.LAUNCHER")
122
+ .toString()
123
+ .split("\n")
124
+ .map(line => line.trim())
125
+ .filter(line => line.startsWith("packageName="))
126
+ .map(line => line.substring("packageName=".length))
127
+ .filter((value, index, self) => self.indexOf(value) === index)
128
+ .map(packageName => ({
129
+ packageName,
130
+ appName: packageName,
131
+ }));
132
+ }
133
+ async listPackages() {
134
+ return this.adb("shell", "pm", "list", "packages")
135
+ .toString()
136
+ .split("\n")
137
+ .map(line => line.trim())
138
+ .filter(line => line.startsWith("package:"))
139
+ .map(line => line.substring("package:".length));
140
+ }
141
+ async launchApp(packageName, locale) {
142
+ (0, utils_1.validatePackageName)(packageName);
143
+ if (locale) {
144
+ (0, utils_1.validateLocale)(locale);
145
+ try {
146
+ this.silentAdb("shell", "cmd", "locale", "set-app-locales", packageName, "--locales", locale);
147
+ }
148
+ catch (error) {
149
+ // set-app-locales requires Android 13+ (API 33), silently ignore on older versions
150
+ }
151
+ }
152
+ try {
153
+ this.silentAdb("shell", "monkey", "-p", packageName, "-c", "android.intent.category.LAUNCHER", "1");
154
+ }
155
+ catch (error) {
156
+ throw new robot_1.ActionableError(`Failed launching app with package name "${packageName}", please make sure it exists`);
157
+ }
158
+ }
159
+ async listRunningProcesses() {
160
+ return this.adb("shell", "ps", "-e")
161
+ .toString()
162
+ .split("\n")
163
+ .map(line => line.trim())
164
+ .filter(line => line.startsWith("u")) // non-system processes
165
+ .map(line => line.split(/\s+/)[8]); // get process name
166
+ }
167
+ async swipe(direction) {
168
+ const screenSize = await this.getScreenSize();
169
+ const centerX = screenSize.width >> 1;
170
+ let x0, y0, x1, y1;
171
+ switch (direction) {
172
+ case "up":
173
+ x0 = x1 = centerX;
174
+ y0 = Math.floor(screenSize.height * 0.80);
175
+ y1 = Math.floor(screenSize.height * 0.20);
176
+ break;
177
+ case "down":
178
+ x0 = x1 = centerX;
179
+ y0 = Math.floor(screenSize.height * 0.20);
180
+ y1 = Math.floor(screenSize.height * 0.80);
181
+ break;
182
+ case "left":
183
+ x0 = Math.floor(screenSize.width * 0.80);
184
+ x1 = Math.floor(screenSize.width * 0.20);
185
+ y0 = y1 = Math.floor(screenSize.height * 0.50);
186
+ break;
187
+ case "right":
188
+ x0 = Math.floor(screenSize.width * 0.20);
189
+ x1 = Math.floor(screenSize.width * 0.80);
190
+ y0 = y1 = Math.floor(screenSize.height * 0.50);
191
+ break;
192
+ default:
193
+ throw new robot_1.ActionableError(`Swipe direction "${direction}" is not supported`);
194
+ }
195
+ this.adb("shell", "input", "swipe", `${x0}`, `${y0}`, `${x1}`, `${y1}`, "1000");
196
+ }
197
+ async swipeFromCoordinate(x, y, direction, distance) {
198
+ const screenSize = await this.getScreenSize();
199
+ let x0, y0, x1, y1;
200
+ // Use provided distance or default to 30% of screen dimension
201
+ const defaultDistanceY = Math.floor(screenSize.height * 0.3);
202
+ const defaultDistanceX = Math.floor(screenSize.width * 0.3);
203
+ const swipeDistanceY = distance || defaultDistanceY;
204
+ const swipeDistanceX = distance || defaultDistanceX;
205
+ switch (direction) {
206
+ case "up":
207
+ x0 = x1 = x;
208
+ y0 = y;
209
+ y1 = Math.max(0, y - swipeDistanceY);
210
+ break;
211
+ case "down":
212
+ x0 = x1 = x;
213
+ y0 = y;
214
+ y1 = Math.min(screenSize.height, y + swipeDistanceY);
215
+ break;
216
+ case "left":
217
+ x0 = x;
218
+ x1 = Math.max(0, x - swipeDistanceX);
219
+ y0 = y1 = y;
220
+ break;
221
+ case "right":
222
+ x0 = x;
223
+ x1 = Math.min(screenSize.width, x + swipeDistanceX);
224
+ y0 = y1 = y;
225
+ break;
226
+ default:
227
+ throw new robot_1.ActionableError(`Swipe direction "${direction}" is not supported`);
228
+ }
229
+ this.adb("shell", "input", "swipe", `${x0}`, `${y0}`, `${x1}`, `${y1}`, "1000");
230
+ }
231
+ getDisplayCount() {
232
+ return this.adb("shell", "dumpsys", "SurfaceFlinger", "--display-id")
233
+ .toString()
234
+ .split("\n")
235
+ .filter(s => s.startsWith("Display "))
236
+ .length;
237
+ }
238
+ getFirstDisplayId() {
239
+ try {
240
+ // Try using cmd display get-displays (Android 11+)
241
+ const displays = this.adb("shell", "cmd", "display", "get-displays")
242
+ .toString()
243
+ .split("\n")
244
+ .filter(s => s.startsWith("Display id "))
245
+ // filter for state ON even though get-displays only returns turned on displays
246
+ .filter(s => s.indexOf(", state ON,") >= 0)
247
+ // another paranoia check
248
+ .filter(s => s.indexOf(", uniqueId ") >= 0);
249
+ if (displays.length > 0) {
250
+ const m = displays[0].match(/uniqueId \"([^\"]+)\"/);
251
+ if (m !== null) {
252
+ let displayId = m[1];
253
+ if (displayId.startsWith("local:")) {
254
+ displayId = displayId.substring("local:".length);
255
+ }
256
+ return displayId;
257
+ }
258
+ }
259
+ }
260
+ catch (error) {
261
+ // cmd display get-displays not available on this device
262
+ }
263
+ // fallback: parse dumpsys display for display info (compatible with older Android versions)
264
+ try {
265
+ const dumpsys = this.adb("shell", "dumpsys", "display")
266
+ .toString();
267
+ // look for DisplayViewport entries with isActive=true and type=INTERNAL
268
+ const viewportMatch = dumpsys.match(/DisplayViewport\{type=INTERNAL[^}]*isActive=true[^}]*uniqueId='([^']+)'/);
269
+ if (viewportMatch) {
270
+ let uniqueId = viewportMatch[1];
271
+ if (uniqueId.startsWith("local:")) {
272
+ uniqueId = uniqueId.substring("local:".length);
273
+ }
274
+ return uniqueId;
275
+ }
276
+ // fallback: look for active display with state ON
277
+ const displayStateMatch = dumpsys.match(/Display Id=(\d+)[\s\S]*?Display State=ON/);
278
+ if (displayStateMatch) {
279
+ return displayStateMatch[1];
280
+ }
281
+ }
282
+ catch (error) {
283
+ // dumpsys display also failed
284
+ }
285
+ return null;
286
+ }
287
+ async getScreenshot() {
288
+ if (this.getDisplayCount() <= 1) {
289
+ // backward compatibility for android 10 and below, and for single display devices
290
+ return this.adb("exec-out", "screencap", "-p");
291
+ }
292
+ // find the first display that is turned on, and capture that one
293
+ const displayId = this.getFirstDisplayId();
294
+ if (displayId === null) {
295
+ // no idea why, but we have displayCount >= 2, yet we failed to parse
296
+ // let's go with screencap's defaults and hope for the best
297
+ return this.adb("exec-out", "screencap", "-p");
298
+ }
299
+ return this.adb("exec-out", "screencap", "-p", "-d", `${displayId}`);
300
+ }
301
+ collectElements(node) {
302
+ const elements = [];
303
+ if (node.node) {
304
+ if (Array.isArray(node.node)) {
305
+ for (const childNode of node.node) {
306
+ elements.push(...this.collectElements(childNode));
307
+ }
308
+ }
309
+ else {
310
+ elements.push(...this.collectElements(node.node));
311
+ }
312
+ }
313
+ if (node.text || node["content-desc"] || node.hint || node["resource-id"] || node.checkable === "true") {
314
+ const element = {
315
+ type: node.class || "text",
316
+ text: node.text,
317
+ label: node["content-desc"] || node.hint || "",
318
+ rect: this.getScreenElementRect(node),
319
+ };
320
+ if (node.focused === "true") {
321
+ // only provide it if it's true, otherwise don't confuse llm
322
+ element.focused = true;
323
+ }
324
+ const resourceId = node["resource-id"];
325
+ if (resourceId !== null && resourceId !== "") {
326
+ element.identifier = resourceId;
327
+ }
328
+ if (element.rect.width > 0 && element.rect.height > 0) {
329
+ elements.push(element);
330
+ }
331
+ }
332
+ return elements;
333
+ }
334
+ async getElementsOnScreen() {
335
+ const parsedXml = await this.getUiAutomatorXml();
336
+ const hierarchy = parsedXml.hierarchy;
337
+ const elements = this.collectElements(hierarchy.node);
338
+ return elements;
339
+ }
340
+ async terminateApp(packageName) {
341
+ (0, utils_1.validatePackageName)(packageName);
342
+ this.adb("shell", "am", "force-stop", packageName);
343
+ }
344
+ async installApp(path) {
345
+ try {
346
+ this.adb("install", "-r", path);
347
+ }
348
+ catch (error) {
349
+ const stdout = error.stdout ? error.stdout.toString() : "";
350
+ const stderr = error.stderr ? error.stderr.toString() : "";
351
+ const output = (stdout + stderr).trim();
352
+ throw new robot_1.ActionableError(output || error.message);
353
+ }
354
+ }
355
+ async uninstallApp(bundleId) {
356
+ try {
357
+ this.adb("uninstall", bundleId);
358
+ }
359
+ catch (error) {
360
+ const stdout = error.stdout ? error.stdout.toString() : "";
361
+ const stderr = error.stderr ? error.stderr.toString() : "";
362
+ const output = (stdout + stderr).trim();
363
+ throw new robot_1.ActionableError(output || error.message);
364
+ }
365
+ }
366
+ async openUrl(url) {
367
+ this.adb("shell", "am", "start", "-a", "android.intent.action.VIEW", "-d", this.escapeShellText(url));
368
+ }
369
+ isAscii(text) {
370
+ return /^[\x00-\x7F]*$/.test(text);
371
+ }
372
+ escapeShellText(text) {
373
+ // escape all shell special characters that could be used for injection
374
+ return text.replace(/[\\'"` \t\n\r|&;()<>{}[\]$*?]/g, "\\$&");
375
+ }
376
+ async isDeviceKitInstalled() {
377
+ const packages = await this.listPackages();
378
+ return packages.includes("com.mobilenext.devicekit");
379
+ }
380
+ async sendKeys(text) {
381
+ if (text === "") {
382
+ // bailing early, so we don't run adb shell with empty string.
383
+ // this happens when you prompt with a simple "submit".
384
+ return;
385
+ }
386
+ if (this.isAscii(text)) {
387
+ // adb shell input only supports ascii characters. and
388
+ // some of the keys have to be escaped.
389
+ const _text = this.escapeShellText(text);
390
+ this.adb("shell", "input", "text", _text);
391
+ }
392
+ else if (await this.isDeviceKitInstalled()) {
393
+ // try sending over clipboard
394
+ const base64 = Buffer.from(text).toString("base64");
395
+ // send clipboard over and immediately paste it
396
+ this.adb("shell", "am", "broadcast", "-a", "devicekit.clipboard.set", "-e", "encoding", "base64", "-e", "text", base64, "-n", "com.mobilenext.devicekit/.ClipboardBroadcastReceiver");
397
+ this.adb("shell", "input", "keyevent", "KEYCODE_PASTE");
398
+ // clear clipboard when we're done
399
+ this.adb("shell", "am", "broadcast", "-a", "devicekit.clipboard.clear", "-n", "com.mobilenext.devicekit/.ClipboardBroadcastReceiver");
400
+ }
401
+ else {
402
+ throw new robot_1.ActionableError("Non-ASCII text is not supported on Android, please install mobilenext devicekit, see https://github.com/mobile-next/devicekit-android");
403
+ }
404
+ }
405
+ async pressButton(button) {
406
+ if (!BUTTON_MAP[button]) {
407
+ throw new robot_1.ActionableError(`Button "${button}" is not supported`);
408
+ }
409
+ const mapped = BUTTON_MAP[button];
410
+ this.adb("shell", "input", "keyevent", mapped);
411
+ }
412
+ async tap(x, y) {
413
+ this.adb("shell", "input", "tap", `${x}`, `${y}`);
414
+ }
415
+ async longPress(x, y, duration) {
416
+ // a long press is a swipe with no movement and a long duration
417
+ this.adb("shell", "input", "swipe", `${x}`, `${y}`, `${x}`, `${y}`, `${duration}`);
418
+ }
419
+ async doubleTap(x, y) {
420
+ await this.tap(x, y);
421
+ await new Promise(r => setTimeout(r, 100)); // short delay
422
+ await this.tap(x, y);
423
+ }
424
+ async setOrientation(orientation) {
425
+ const value = orientation === "portrait" ? 0 : 1;
426
+ // disable auto-rotation prior to setting the orientation
427
+ this.adb("shell", "settings", "put", "system", "accelerometer_rotation", "0");
428
+ this.adb("shell", "content", "insert", "--uri", "content://settings/system", "--bind", "name:s:user_rotation", "--bind", `value:i:${value}`);
429
+ }
430
+ async getOrientation() {
431
+ const rotation = this.adb("shell", "settings", "get", "system", "user_rotation").toString().trim();
432
+ return rotation === "0" ? "portrait" : "landscape";
433
+ }
434
+ /**
435
+ * 获取设备日志 (logcat)
436
+ * @param options - 可选的过滤选项
437
+ * @param options.bundleName - 按日志标签过滤 (格式: tag:level 或 tag)
438
+ * @param options.pid - 按进程 ID 过滤
439
+ * @param options.level - 最低日志级别 (V/D/I/W/E/F)
440
+ * @param options.lines - 获取最近 N 行日志 (默认: 100)
441
+ * @param options.clear - 获取前清除日志缓冲区
442
+ */
443
+ async getDeviceLogs(options) {
444
+ try {
445
+ // 如果需要清除日志
446
+ if (options?.clear) {
447
+ this.adb("logcat", "-c");
448
+ }
449
+ // 构建 logcat 命令参数
450
+ const args = ["logcat", "-d"]; // -d 表示 dump 后退出
451
+ // 添加行数限制
452
+ const lines = options?.lines || 100;
453
+ args.push("-t", lines.toString());
454
+ // 添加日志级别过滤
455
+ if (options?.level) {
456
+ args.push("*:" + options.level.toUpperCase());
457
+ }
458
+ // 添加标签过滤
459
+ if (options?.bundleName) {
460
+ args.push("-s", options.bundleName);
461
+ }
462
+ // 添加进程 ID 过滤
463
+ if (options?.pid) {
464
+ args.push("--pid", options.pid.toString());
465
+ }
466
+ const output = this.adb(...args).toString();
467
+ return output;
468
+ }
469
+ catch (error) {
470
+ throw new robot_1.ActionableError(`Failed to get device logs: ${error.message}`);
471
+ }
472
+ }
473
+ async getUiAutomatorDump() {
474
+ for (let tries = 0; tries < 10; tries++) {
475
+ const dump = this.adb("exec-out", "uiautomator", "dump", "/dev/tty").toString();
476
+ // note: we're not catching other errors here. maybe we should check for <?xml
477
+ if (dump.includes("null root node returned by UiTestAutomationBridge")) {
478
+ // uncomment for debugging
479
+ // const screenshot = await this.getScreenshot();
480
+ // console.error("Failed to get UIAutomator XML. Here's a screenshot: " + screenshot.toString("base64"));
481
+ continue;
482
+ }
483
+ return dump.substring(dump.indexOf("<?xml"));
484
+ }
485
+ throw new robot_1.ActionableError("Failed to get UIAutomator XML");
486
+ }
487
+ async getUiAutomatorXml() {
488
+ const dump = await this.getUiAutomatorDump();
489
+ const parser = new xml.XMLParser({
490
+ ignoreAttributes: false,
491
+ attributeNamePrefix: "",
492
+ });
493
+ return parser.parse(dump);
494
+ }
495
+ getScreenElementRect(node) {
496
+ const bounds = String(node.bounds);
497
+ const [, left, top, right, bottom] = bounds.match(/^\[(\d+),(\d+)\]\[(\d+),(\d+)\]$/)?.map(Number) || [];
498
+ return {
499
+ x: left,
500
+ y: top,
501
+ width: right - left,
502
+ height: bottom - top,
503
+ };
504
+ }
505
+ }
506
+ exports.AndroidRobot = AndroidRobot;
507
+ class AndroidDeviceManager {
508
+ getDeviceType(name) {
509
+ try {
510
+ const device = new AndroidRobot(name);
511
+ const features = device.getSystemFeatures();
512
+ if (features.includes("android.software.leanback") || features.includes("android.hardware.type.television")) {
513
+ return "tv";
514
+ }
515
+ return "mobile";
516
+ }
517
+ catch (error) {
518
+ // Fallback to mobile if we cannot determine device type
519
+ return "mobile";
520
+ }
521
+ }
522
+ getDeviceVersion(deviceId) {
523
+ try {
524
+ const output = (0, node_child_process_1.execFileSync)(getAdbPath(), ["-s", deviceId, "shell", "getprop", "ro.build.version.release"], {
525
+ timeout: 5000,
526
+ }).toString().trim();
527
+ return output;
528
+ }
529
+ catch (error) {
530
+ return "unknown";
531
+ }
532
+ }
533
+ getDeviceName(deviceId) {
534
+ try {
535
+ // Try getting AVD name first (for emulators)
536
+ const avdName = (0, node_child_process_1.execFileSync)(getAdbPath(), ["-s", deviceId, "shell", "getprop", "ro.boot.qemu.avd_name"], {
537
+ timeout: 5000,
538
+ }).toString().trim();
539
+ if (avdName !== "") {
540
+ // Replace underscores with spaces (e.g., "Pixel_9_Pro" -> "Pixel 9 Pro")
541
+ return avdName.replace(/_/g, " ");
542
+ }
543
+ // Fall back to product model
544
+ const output = (0, node_child_process_1.execFileSync)(getAdbPath(), ["-s", deviceId, "shell", "getprop", "ro.product.model"], {
545
+ timeout: 5000,
546
+ }).toString().trim();
547
+ return output;
548
+ }
549
+ catch (error) {
550
+ return deviceId;
551
+ }
552
+ }
553
+ getConnectedDevices() {
554
+ try {
555
+ const names = (0, node_child_process_1.execFileSync)(getAdbPath(), ["devices"])
556
+ .toString()
557
+ .split("\n")
558
+ .map(line => line.trim())
559
+ .filter(line => line !== "")
560
+ .filter(line => !line.startsWith("List of devices attached"))
561
+ .filter(line => line.split("\t")[1]?.trim() === "device") // Only include devices that are online and ready
562
+ .map(line => line.split("\t")[0]);
563
+ return names.map(name => ({
564
+ deviceId: name,
565
+ deviceType: this.getDeviceType(name),
566
+ }));
567
+ }
568
+ catch (error) {
569
+ console.error("Could not execute adb command, maybe ANDROID_HOME is not set?");
570
+ return [];
571
+ }
572
+ }
573
+ getConnectedDevicesWithDetails() {
574
+ try {
575
+ const names = (0, node_child_process_1.execFileSync)(getAdbPath(), ["devices"])
576
+ .toString()
577
+ .split("\n")
578
+ .map(line => line.trim())
579
+ .filter(line => line !== "")
580
+ .filter(line => !line.startsWith("List of devices attached"))
581
+ .filter(line => line.split("\t")[1]?.trim() === "device") // Only include devices that are online and ready
582
+ .map(line => line.split("\t")[0]);
583
+ return names.map(deviceId => ({
584
+ deviceId,
585
+ deviceType: this.getDeviceType(deviceId),
586
+ version: this.getDeviceVersion(deviceId),
587
+ name: this.getDeviceName(deviceId),
588
+ }));
589
+ }
590
+ catch (error) {
591
+ console.error("Could not execute adb command, maybe ANDROID_HOME is not set?");
592
+ return [];
593
+ }
594
+ }
595
+ }
596
+ exports.AndroidDeviceManager = AndroidDeviceManager;