rfi-ai-android
Version:
Android automation library for Midscene
1,057 lines (1,049 loc) • 33.8 kB
JavaScript
// 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
};