UNPKG

rfi-ai-android

Version:

Android automation library for Midscene

1,057 lines (1,049 loc) 33.8 kB
// src/agent/index.ts import { PageAgent } from "rfi-ai-web/agent"; // src/page/index.ts import assert from "assert"; import fs from "fs"; import { getAIConfig } from "rfi-ai-core"; import { getTmpFile, sleep } from "rfi-ai-core/utils"; import { MIDSCENE_ANDROID_IME_STRATEGY } from "rfi-ai-shared/env"; import { isValidPNGImageBuffer, resizeImg } from "rfi-ai-shared/img"; import { getDebug } from "rfi-ai-shared/logger"; import { repeat } from "rfi-ai-shared/utils"; import { remote } from "webdriverio"; var androidScreenshotPath = "/data/local/tmp/midscene_screenshot.png"; var defaultScrollUntilTimes = 10; var defaultFastScrollDuration = 100; var debugPage = getDebug("android:device"); var AndroidDevice = class { constructor(deviceId, options) { this.screenSize = null; this.deviceRatio = 1; this.driver = null; this.connectingDriver = null; this.destroyed = false; this.pageType = "android"; assert(deviceId, "deviceId is required for AndroidDevice"); this.deviceId = deviceId; this.options = options; } /** * Connects to the Appium server and starts a session */ async connect() { if (this.driver) { debugPage("Already connected to Appium server"); return this.driver; } try { const defaultPort = 4723; const defaultHost = "127.0.0.1"; debugPage( "Connecting to Appium server at %s://%s:%d%s", this.options?.protocol || "http", this.options?.hostname || defaultHost, this.options?.port || defaultPort, this.options?.path || "/wd/hub" ); const options = { hostname: this.options?.hostname || defaultHost, port: this.options?.port || defaultPort, path: this.options?.path || "/wd/hub", protocol: this.options?.protocol || "http", capabilities: this.options?.capabilities, logLevel: "info", connectionRetryTimeout: 12e4, connectionRetryCount: 3 }; debugPage( "Starting Appium session with capabilities: %O", this.options?.capabilities ); this.driver = await remote(options); debugPage( "Successfully connected to Appium server, session ID: %s", this.driver.sessionId ); const size = await this.getScreenSize(); console.log(` DeviceId: ${this.deviceId} ScreenSize: ${Object.keys(size).filter((key) => size[key]).map( (key) => ` ${key} size: ${size[key]}${key === "override" && size[key] ? " ✅" : ""}` ).join("\n")} `); debugPage("WebDriverIO initialized successfully"); return this.driver; } catch (error) { debugPage("Failed to connect to Appium server: %s", error.message); throw new Error(`Failed to connect to Appium server: ${error.message}`, { cause: error }); } } /** * Disconnects from the Appium server and ends the session */ async disconnect() { if (!this.driver) { debugPage("No active Appium session to disconnect"); return; } try { debugPage("Ending Appium session"); const isSauceLabs = this.options?.hostname?.includes("saucelabs.com"); if (isSauceLabs) { debugPage("Detected Sauce Labs session, ensuring proper termination"); try { await this.driver.executeScript("sauce:job-result", [ { passed: true } ]); } catch (e) { debugPage( "Could not set Sauce Labs job result: %s", e.message ); } await this.driver.deleteSession(); } else { await this.driver.deleteSession(); } this.driver = null; debugPage("Successfully ended Appium session"); } catch (error) { debugPage("Error ending Appium session: %s", error.message); throw new Error(`Failed to end Appium session: ${error.message}`, { cause: error }); } } async getDriver() { if (this.destroyed) { throw new Error( `AndroidDevice ${this.deviceId} has been destroyed and cannot execute WebDriverIO commands` ); } if (this.driver) { return this.driver; } if (this.connectingDriver) { return this.connectingDriver; } this.connectingDriver = (async () => { let error = null; debugPage(`Initializing WebDriverIO with device ID: ${this.deviceId}`); try { await this.connect(); return this.driver; } catch (e) { debugPage(`Failed to initialize WebDriverIO: ${e}`); error = new Error(`Unable to connect to device ${this.deviceId}: ${e}`); } finally { this.connectingDriver = null; } if (error) { throw error; } throw new Error("WebDriverIO initialization failed unexpectedly"); })(); return this.connectingDriver; } async launch(uri) { const driver = await this.getDriver(); this.uri = uri; try { if (uri.startsWith("http://") || uri.startsWith("https://") || uri.includes("://")) { await driver.url(uri); } else if (uri.includes("/")) { const [appPackage, appActivity] = uri.split("/"); await driver.startActivity(appPackage, appActivity); } else { await driver.activateApp(uri); } debugPage(`Successfully launched: ${uri}`); } catch (error) { debugPage(`Error launching ${uri}: ${error}`); throw new Error(`Failed to launch ${uri}: ${error.message}`, { cause: error }); } return this; } // @deprecated async getElementsInfo() { return []; } async getElementsNodeTree() { return { node: null, children: [] }; } async getScreenSize() { const driver = await this.getDriver(); const windowSize = await driver.getWindowSize(); const orientation = await driver.getOrientation(); const orientationMap = { "PORTRAIT": 0, "LANDSCAPE": 1, "PORTRAIT_UPSIDE_DOWN": 2, "LANDSCAPE_LEFT": 3 }; const size = { override: `${windowSize.width}x${windowSize.height}`, physical: `${windowSize.width}x${windowSize.height}`, orientation: orientationMap[orientation] || 0 }; debugPage(`Using screen size: ${size.override}, orientation: ${size.orientation}`); return size; } async size() { if (this.screenSize) { return this.screenSize; } const driver = await this.getDriver(); const windowSize = await driver.getWindowSize(); const orientation = await driver.getOrientation(); this.deviceRatio = 1; this.screenSize = { width: windowSize.width, height: windowSize.height }; return this.screenSize; } async screenshotBase64() { debugPage("screenshotBase64 begin"); const { width, height } = await this.size(); const driver = await this.getDriver(); let screenshotBuffer; try { const screenshotBase64 = await driver.takeScreenshot(); screenshotBuffer = Buffer.from(screenshotBase64, "base64"); if (!screenshotBuffer) { throw new Error( "Failed to capture screenshot: screenshotBuffer is null" ); } if (!isValidPNGImageBuffer(screenshotBuffer)) { debugPage("Invalid image buffer detected: not a valid image format"); throw new Error( "Screenshot buffer has invalid format: could not find valid image signature" ); } } catch (error) { const screenshotPath = getTmpFile("png"); try { await driver.execute("mobile: shell", { command: "screencap", args: ["-p", androidScreenshotPath] }); } catch (error2) { throw new Error("Unable to take screenshot using available methods"); } await driver.execute("mobile: pullFile", { remotePath: androidScreenshotPath, localPath: screenshotPath }); screenshotBuffer = await fs.promises.readFile(screenshotPath); } const resizedScreenshotBuffer = await resizeImg(screenshotBuffer, { width, height }); const result = `data:image/jpeg;base64,${resizedScreenshotBuffer.toString("base64")}`; debugPage("screenshotBase64 end"); return result; } adjustCoordinates(x, y) { const ratio = this.deviceRatio; return { x: Math.round(x * ratio), y: Math.round(y * ratio) }; } reverseAdjustCoordinates(x, y) { const ratio = this.deviceRatio; return { x: Math.round(x / ratio), y: Math.round(y / ratio) }; } get mouse() { return { click: (x, y) => this.mouseClick(x, y), wheel: async (deltaX, deltaY) => { if (Math.abs(deltaY) > Math.abs(deltaX)) { if (deltaY > 0) { await this.scrollDown(); } else { await this.scrollUp(); } } else { if (deltaX > 0) { await this.scrollRight(); } else { await this.scrollLeft(); } } }, move: (x, y) => this.mouseMove(x, y), drag: (from, to) => this.swipe(from.x, from.y, to.x, to.y) }; } get keyboard() { return { type: (text, options) => this.keyboardType(text, options), press: (action) => this.keyboardPressAction(action) }; } async clearInput(element) { if (!element) { return; } const driver = await this.getDriver(); await this.mouse.click(element.center[0], element.center[1]); try { await driver.sendKeys(["Meta", "a"]); await driver.sendKeys("Delete"); } catch (error) { debugPage("Standard clear failed, using backspace fallback"); for (let i = 0; i < 50; i++) { await driver.pressKeyCode(67); } } if (await driver.isKeyboardShown()) { return; } await this.mouse.click(element.center[0], element.center[1]); } async url() { return ""; } async scrollUntilTop(startPoint) { if (startPoint) { const startX = startPoint.left; const startY = startPoint.top; const endX = startX; const endY = 0; await this.swipe(startX, startY, endX, endY, 1200); return; } await repeat(defaultScrollUntilTimes, async () => { const { width, height } = await this.size(); await this.swipe(width / 2, height * 0.8, width / 2, height * 0.1, defaultFastScrollDuration); }); await sleep(1e3); } async scrollUntilBottom(startPoint) { if (startPoint) { const { height } = await this.size(); const startX = startPoint.left; const startY = startPoint.top; const endX = startX; const endY = height; await this.swipe(startX, startY, endX, endY, 1200); return; } await repeat(defaultScrollUntilTimes, async () => { const { width, height } = await this.size(); await this.swipe(width / 2, height * 0.2, width / 2, height * 0.9, defaultFastScrollDuration); }); await sleep(1e3); } async scrollUntilLeft(startPoint) { if (startPoint) { const startX = startPoint.left; const startY = startPoint.top; const endX = 0; const endY = startY; await this.swipe(startX, startY, endX, endY, 1200); return; } await repeat(defaultScrollUntilTimes, async () => { const { width, height } = await this.size(); await this.swipe(width * 0.8, height / 2, width * 0.1, height / 2, defaultFastScrollDuration); }); await sleep(1e3); } async scrollUntilRight(startPoint) { if (startPoint) { const { width } = await this.size(); const startX = startPoint.left; const startY = startPoint.top; const endX = width; const endY = startY; await this.swipe(startX, startY, endX, endY, 1200); return; } await repeat(defaultScrollUntilTimes, async () => { const { width, height } = await this.size(); await this.swipe(width * 0.2, height / 2, width * 0.9, height / 2, defaultFastScrollDuration); }); await sleep(1e3); } async scrollUp(distance, startPoint) { const { width, height } = await this.size(); const scrollDistance = distance || Math.floor(height * 0.7); if (startPoint) { const startX2 = startPoint.left; const startY2 = startPoint.top; const endX2 = startX2; const endY2 = Math.min(height, startY2 + scrollDistance); await this.swipe(startX2, startY2, endX2, endY2); return; } const startX = width / 2; const startY = height * 0.2; const endX = startX; const endY = Math.min(height * 0.8, startY + scrollDistance); await this.swipe(startX, startY, endX, endY); } async scrollDown(distance, startPoint) { const { width, height } = await this.size(); const scrollDistance = distance || Math.floor(height * 0.7); if (startPoint) { const startX2 = startPoint.left; const startY2 = startPoint.top; const endX2 = startX2; const endY2 = Math.max(0, startY2 - scrollDistance); await this.swipe(startX2, startY2, endX2, endY2); return; } const startX = width / 2; const startY = height * 0.8; const endX = startX; const endY = Math.max(height * 0.2, startY - scrollDistance); await this.swipe(startX, startY, endX, endY); } async scrollLeft(distance, startPoint) { const { width, height } = await this.size(); const scrollDistance = distance || Math.floor(width * 0.7); if (startPoint) { const startX2 = startPoint.left; const startY2 = startPoint.top; const endX2 = Math.max(0, startX2 - scrollDistance); const endY2 = startY2; await this.swipe(startX2, startY2, endX2, endY2); return; } const startX = width * 0.8; const startY = height / 2; const endX = Math.max(width * 0.2, startX - scrollDistance); const endY = startY; await this.swipe(startX, startY, endX, endY); } async scrollRight(distance, startPoint) { const { width, height } = await this.size(); const scrollDistance = distance || Math.floor(width * 0.7); if (startPoint) { const startX2 = startPoint.left; const startY2 = startPoint.top; const endX2 = Math.min(width, startX2 + scrollDistance); const endY2 = startY2; await this.swipe(startX2, startY2, endX2, endY2); return; } const startX = width * 0.2; const startY = height / 2; const endX = Math.min(width * 0.8, startX + scrollDistance); const endY = startY; await this.swipe(startX, startY, endX, endY); } async keyboardType(text, options) { if (!text) return; const driver = await this.getDriver(); const IME_STRATEGY = (this.options?.imeStrategy || getAIConfig(MIDSCENE_ANDROID_IME_STRATEGY)) ?? "webdriverio-only"; const isAutoDismissKeyboard = options?.autoDismissKeyboard ?? this.options?.autoDismissKeyboard ?? true; await driver.sendKeys(text); if (isAutoDismissKeyboard === true) { await driver.hideKeyboard(); } } async keyboardPress(key) { const keyCodeMap = { Enter: 66, Backspace: 67, Tab: 61, ArrowUp: 19, ArrowDown: 20, ArrowLeft: 21, ArrowRight: 22, Escape: 111, Home: 3, End: 123 }; const driver = await this.getDriver(); const keyCode = keyCodeMap[key]; if (keyCode !== void 0) { await driver.pressKeyCode(keyCode); } else { if (key.length === 1) { const asciiCode = key.toUpperCase().charCodeAt(0); if (asciiCode >= 65 && asciiCode <= 90) { await driver.pressKeyCode(asciiCode - 36); } } } } async keyboardPressAction(action) { if (Array.isArray(action)) { for (const act of action) { await this.keyboardPress(act.key); } } else { await this.keyboardPress(action.key); } } async mouseClick(x, y) { const driver = await this.getDriver(); const { x: adjustedX, y: adjustedY } = this.adjustCoordinates(x, y); await driver.performActions([ { type: "pointer", id: "finger1", parameters: { pointerType: "touch" }, actions: [ { type: "pointerMove", x: adjustedX, y: adjustedY, duration: 0 }, { type: "pointerDown", button: 0 }, { type: "pause", duration: 50 }, { type: "pointerUp", button: 0 } ] } ]); await driver.releaseActions(); } async mouseMove(x, y) { return Promise.resolve(); } /** * @deprecated Use swipe() method instead for W3C Actions API compliance */ async mouseDrag(from, to) { await this.swipe(from.x, from.y, to.x, to.y); } async destroy() { if (this.destroyed) { return; } this.destroyed = true; try { if (this.driver) { await this.driver.execute("mobile: shell", { command: "rm", args: ["-f", androidScreenshotPath] }); } await this.disconnect(); } catch (error) { console.error("Error during cleanup:", error); } this.connectingDriver = null; this.screenSize = null; } async back() { const driver = await this.getDriver(); await driver.back(); } async home() { const driver = await this.getDriver(); await driver.pressKeyCode(3); } async recentApps() { const driver = await this.getDriver(); await driver.pressKeyCode(82); } async getXpathsById(id) { throw new Error("Not implemented"); } async getElementInfoByXpath(xpath) { throw new Error("Not implemented"); } /** * W3C Actions API - Tap at specific coordinates */ async tap(x, y) { return this.mouseClick(x, y); } /** * W3C Actions API - Swipe from one point to another */ async swipe(startX, startY, endX, endY, duration = 800) { const driver = await this.getDriver(); const { x: adjustedStartX, y: adjustedStartY } = this.adjustCoordinates(startX, startY); const { x: adjustedEndX, y: adjustedEndY } = this.adjustCoordinates(endX, endY); await driver.performActions([ { type: "pointer", id: "finger1", parameters: { pointerType: "touch" }, actions: [ { type: "pointerMove", x: adjustedStartX, y: adjustedStartY, duration: 0 }, { type: "pointerDown", button: 0 }, { type: "pause", duration: 50 }, { type: "pointerMove", x: adjustedEndX, y: adjustedEndY, duration }, { type: "pointerUp", button: 0 } ] } ]); await driver.releaseActions(); } }; // src/agent/index.ts import { vlLocateMode } from "rfi-ai-shared/env"; // src/utils/media.ts import { existsSync, mkdirSync, writeFileSync } from "fs"; import * as path from "path"; import { getDebug as getDebug2 } from "rfi-ai-shared/logger"; var debugMedia = getDebug2("android:utils:media"); async function takeScreenshot(device, options = {}) { const { filePath, createDir = true, returnBase64 = true } = options; debugMedia("Taking screenshot"); try { const base64Screenshot = await device.screenshotBase64(); if (filePath) { debugMedia(`Saving screenshot to ${filePath}`); if (createDir) { const dir = path.dirname(filePath); if (!existsSync(dir)) { debugMedia(`Creating directory ${dir}`); mkdirSync(dir, { recursive: true }); } } const base64Data = base64Screenshot.replace(/^data:image\/\w+;base64,/, ""); const buffer = Buffer.from(base64Data, "base64"); writeFileSync(filePath, buffer); debugMedia(`Screenshot saved to ${filePath}`); } if (returnBase64) { return base64Screenshot; } } catch (error) { debugMedia(`Error taking screenshot: ${error.message}`); throw new Error(`Failed to take screenshot: ${error.message}`, { cause: error }); } } async function startVideoRecording(device, options = {}) { const { timeLimit = 180, bitRate = 4e6, size = "1280x720" } = options; debugMedia("Starting video recording"); try { const driver = await device.getDriver(); await driver.startRecordingScreen({ timeLimit, videoSize: size, bitRate }); debugMedia("Video recording started"); } catch (error) { debugMedia(`Error starting video recording: ${error.message}`); throw new Error(`Failed to start video recording: ${error.message}`, { cause: error }); } } async function stopVideoRecording(device, options) { const { filePath, createDir = true } = options; debugMedia("Stopping video recording"); try { const driver = await device.getDriver(); const base64Video = await driver.stopRecordingScreen(); if (createDir) { const dir = path.dirname(filePath); if (!existsSync(dir)) { debugMedia(`Creating directory ${dir}`); mkdirSync(dir, { recursive: true }); } } const buffer = Buffer.from(base64Video, "base64"); writeFileSync(filePath, buffer); debugMedia(`Video saved to ${filePath}`); return base64Video; } catch (error) { debugMedia(`Error stopping video recording: ${error.message}`); throw new Error(`Failed to stop video recording: ${error.message}`, { cause: error }); } } // src/utils/index.ts async function getConnectedDevices() { return ["emulator-5554", "device-1234"]; } // src/agent/index.ts import { getDebug as getDebug3 } from "rfi-ai-shared/logger"; var debugDevice = getDebug3("android-device"); var AndroidAgent = class extends PageAgent { constructor(page, opts) { super(page, opts); if (!vlLocateMode()) { throw new Error( "Android Agent only supports vl-model. RFI-AI" ); } } async launch(uri) { const device = this.page; await device.launch(uri); } }; var AppiumDevice = class extends AndroidDevice { constructor(config, capabilities) { super("appium-device", { hostname: config.hostname, port: config.port, protocol: config.protocol, path: config.path, capabilities }); this.config = config; this.capabilities = capabilities; } async connect() { debugDevice( "Connecting to Appium server at %s://%s:%s", this.config.protocol, this.config.hostname, this.config.port ); await super.connect(); } async getCurrentPackage() { const driver = await this.getDriver(); try { const currentActivity = await driver.getCurrentActivity(); const currentPackage = await driver.getCurrentPackage(); return currentPackage || "unknown"; } catch (error) { debugDevice("Error getting current package: %s", error.message); return "unknown"; } } async getDriver() { return super.getDriver(); } }; async function agentFromAdbDevice(deviceId, opts) { if (!deviceId) { const devices = await getConnectedDevices(); if (devices.length === 0) { throw new Error("No connected Android devices found"); } deviceId = devices[0]; } const page = new AndroidDevice(deviceId, { autoDismissKeyboard: opts?.autoDismissKeyboard, imeStrategy: opts?.imeStrategy }); return new AndroidAgent(page, opts); } async function agentFromAppiumServer(config, capabilities, agentOpts) { const device = new AppiumDevice(config, capabilities); try { await device.connect(); return new AndroidAgent(device, agentOpts); } catch (error) { debugDevice("Failed to connect to Appium server: %s", error.message); throw new Error(`Failed to connect to Appium server: ${error.message}`, { cause: error }); } } async function agentFromLocalAppium(capabilities, agentOpts) { const localServerConfig = { hostname: "127.0.0.1", port: 4723, protocol: "http" }; return agentFromAppiumServer(localServerConfig, capabilities, agentOpts); } async function agentFromSauceLabs(slConfig, capabilities, agentOpts) { const sauceServerConfig = { hostname: `ondemand.${slConfig.region}.saucelabs.com`, port: 443, protocol: "https", path: "/wd/hub" }; if (!capabilities["sauce:options"]) { capabilities["sauce:options"] = {}; } capabilities["sauce:options"].username = slConfig.user; capabilities["sauce:options"].accessKey = slConfig.key; return agentFromAppiumServer(sauceServerConfig, capabilities, agentOpts); } // src/index.ts import { overrideAIConfig } from "rfi-ai-shared/env"; // src/performance/index.ts import { getDebug as getDebug4 } from "rfi-ai-shared/logger"; var debugDevice2 = getDebug4("android-device"); var PerformanceMonitor = class { /** * Creates a new PerformanceMonitor instance * * @param device - AppiumDevice instance * @param defaultPackageName - Optional default package name to use if active package detection fails */ constructor(device, defaultPackageName) { this.metrics = []; this.monitoringInterval = null; this.availableMetrics = []; this.lastActivePackage = ""; this.device = device; this.defaultPackageName = defaultPackageName; } /** * Gets the currently active package name */ async getActivePackage() { try { const currentPackage = await this.device.getCurrentPackage(); if (currentPackage) { this.lastActivePackage = currentPackage; return currentPackage; } } catch (error) { debugDevice2("Error getting active package: %s", error.message); } if (this.lastActivePackage) { return this.lastActivePackage; } if (this.defaultPackageName) { return this.defaultPackageName; } return "unknown.package"; } /** * Initializes the performance monitor */ async initialize() { debugDevice2("Initializing performance monitor"); const driver = await this.device.getDriver(); this.availableMetrics = await driver.getPerformanceDataTypes(); debugDevice2("Available performance metrics: %O", this.availableMetrics); return this.availableMetrics; } /** * Gets device information */ async getDeviceInfo() { debugDevice2("Getting device information"); const driver = await this.device.getDriver(); const executeShellCommand = async (command) => { return await driver.executeScript("mobile: shell", [{ command }]); }; const model = await executeShellCommand("getprop ro.product.model"); const manufacturer = await executeShellCommand("getprop ro.product.manufacturer"); const androidVersion = await executeShellCommand("getprop ro.build.version.release"); const cpuArchitecture = await executeShellCommand("uname -m"); const cpuCores = parseInt(await executeShellCommand("cat /proc/cpuinfo | grep processor | wc -l"), 10); const totalRam = await executeShellCommand("cat /proc/meminfo | grep MemTotal"); const screenDensity = await executeShellCommand("wm density"); const deviceInfo = { model: model.trim(), manufacturer: manufacturer.trim(), androidVersion: androidVersion.trim(), cpuArchitecture: cpuArchitecture.trim(), cpuCores: isNaN(cpuCores) ? 0 : cpuCores, totalRam: totalRam.trim(), screenDensity: screenDensity.trim() }; debugDevice2("Device information: %O", deviceInfo); return deviceInfo; } /** * Gets current performance metrics */ async getCurrentMetrics() { debugDevice2("Getting current performance metrics"); const driver = await this.device["getDriver"](); const activePackage = await this.getActivePackage(); debugDevice2("Getting performance metrics for active package: %s", activePackage); const metrics = { timestamp: Date.now(), packageName: activePackage }; try { if (this.availableMetrics.includes("cpuinfo")) { const cpuData = await driver.getPerformanceData(activePackage, "cpuinfo", 1); if (cpuData && cpuData.length > 1) { const headers = cpuData[0]; const values = cpuData[1]; const userIndex = headers.indexOf("user"); const systemIndex = headers.indexOf("system"); const idleIndex = headers.indexOf("idle"); const totalIndex = headers.indexOf("total"); metrics.cpuInfo = { user: userIndex >= 0 ? parseFloat(values[userIndex]) : 0, system: systemIndex >= 0 ? parseFloat(values[systemIndex]) : 0, idle: idleIndex >= 0 ? parseFloat(values[idleIndex]) : 0, total: totalIndex >= 0 ? parseFloat(values[totalIndex]) : 0 }; } } if (this.availableMetrics.includes("memoryinfo")) { const memData = await driver.getPerformanceData(activePackage, "memoryinfo", 1); if (memData && memData.length > 1) { const headers = memData[0]; const values = memData[1]; const totalIndex = headers.indexOf("totalMem"); const freeIndex = headers.indexOf("freeMem"); const totalMem = totalIndex >= 0 ? parseInt(values[totalIndex], 10) : 0; const freeMem = freeIndex >= 0 ? parseInt(values[freeIndex], 10) : 0; const usedMem = totalMem - freeMem; const usedMemPercent = totalMem > 0 ? usedMem / totalMem * 100 : 0; metrics.memoryInfo = { totalMem, freeMem, usedMem, usedMemPercent }; } } if (this.availableMetrics.includes("batteryinfo")) { const batteryData = await driver.getPerformanceData(activePackage, "batteryinfo", 1); if (batteryData && batteryData.length > 1) { const headers = batteryData[0]; const values = batteryData[1]; const levelIndex = headers.indexOf("level"); const statusIndex = headers.indexOf("status"); const tempIndex = headers.indexOf("temperature"); metrics.batteryInfo = { level: levelIndex >= 0 ? parseInt(values[levelIndex], 10) : 0, status: statusIndex >= 0 ? values[statusIndex] : "", temperature: tempIndex >= 0 ? parseInt(values[tempIndex], 10) / 10 : 0 }; } } if (this.availableMetrics.includes("networkinfo")) { const networkData = await driver.getPerformanceData(activePackage, "networkinfo", 1); if (networkData && networkData.length > 1) { const headers = networkData[0]; const values = networkData[1]; const rxBytesIndex = headers.indexOf("rxBytes"); const txBytesIndex = headers.indexOf("txBytes"); const rxPacketsIndex = headers.indexOf("rxPackets"); const txPacketsIndex = headers.indexOf("txPackets"); metrics.networkInfo = { rxBytes: rxBytesIndex >= 0 ? parseInt(values[rxBytesIndex], 10) : 0, txBytes: txBytesIndex >= 0 ? parseInt(values[txBytesIndex], 10) : 0, rxPackets: rxPacketsIndex >= 0 ? parseInt(values[rxPacketsIndex], 10) : 0, txPackets: txPacketsIndex >= 0 ? parseInt(values[txPacketsIndex], 10) : 0 }; } } } catch (error) { debugDevice2("Error getting performance metrics: %s", error.message); } this.metrics.push(metrics); return metrics; } /** * Starts monitoring performance metrics at the specified interval * * @param intervalMs - Interval in milliseconds (default: 5000) */ startMonitoring(intervalMs = 5e3) { if (this.monitoringInterval) { this.stopMonitoring(); } debugDevice2("Starting performance monitoring with interval %d ms", intervalMs); this.monitoringInterval = setInterval(async () => { await this.getCurrentMetrics(); }, intervalMs); } /** * Stops monitoring performance metrics */ stopMonitoring() { if (this.monitoringInterval) { debugDevice2("Stopping performance monitoring"); clearInterval(this.monitoringInterval); this.monitoringInterval = null; } } /** * Gets all collected metrics */ getMetrics() { return this.metrics; } /** * Clears all collected metrics */ clearMetrics() { this.metrics = []; } /** * Exports metrics to JSON string */ exportMetricsToJson() { return JSON.stringify(this.metrics, null, 2); } /** * Exports metrics to a JSON file * * @param filePath - Path to save the JSON file */ async exportMetricsToFile(filePath) { const fs2 = await import("fs/promises"); const path2 = await import("path"); try { const dir = path2.dirname(filePath); await fs2.mkdir(dir, { recursive: true }); const jsonData = this.exportMetricsToJson(); await fs2.writeFile(filePath, jsonData, "utf8"); debugDevice2("Performance metrics exported to %s", filePath); } catch (error) { debugDevice2("Error exporting metrics to file: %s", error.message); throw new Error(`Failed to export metrics to file: ${error.message}`, { cause: error }); } } /** * Gets metrics for a specific package * * @param packageName - Package name to filter by */ getMetricsForPackage(packageName) { return this.metrics.filter((metric) => metric.packageName === packageName); } /** * Gets metrics within a time range * * @param startTime - Start timestamp in milliseconds * @param endTime - End timestamp in milliseconds */ getMetricsInTimeRange(startTime, endTime) { return this.metrics.filter( (metric) => metric.timestamp >= startTime && metric.timestamp <= endTime ); } }; export { AndroidAgent, AndroidDevice, AndroidDevice as AndroidDeviceBase, AppiumDevice, PerformanceMonitor, agentFromAdbDevice, agentFromAppiumServer, agentFromLocalAppium, agentFromSauceLabs, overrideAIConfig, startVideoRecording, stopVideoRecording, takeScreenshot };