rfi-ai-android
Version:
Android automation library for Midscene
1,061 lines (1,051 loc) • 35 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
AndroidAgent: () => AndroidAgent,
AndroidDevice: () => AndroidDevice,
AndroidDeviceBase: () => AndroidDevice,
AppiumDevice: () => AppiumDevice,
PerformanceMonitor: () => PerformanceMonitor,
agentFromAdbDevice: () => agentFromAdbDevice,
agentFromAppiumServer: () => agentFromAppiumServer,
agentFromLocalAppium: () => agentFromLocalAppium,
agentFromSauceLabs: () => agentFromSauceLabs,
overrideAIConfig: () => import_env3.overrideAIConfig,
startVideoRecording: () => startVideoRecording,
stopVideoRecording: () => stopVideoRecording,
takeScreenshot: () => takeScreenshot
});
module.exports = __toCommonJS(src_exports);
// src/agent/index.ts
var import_agent = require("rfi-ai-web/agent");
// src/page/index.ts
var import_node_assert = __toESM(require("assert"));
var import_node_fs = __toESM(require("fs"));
var import_rfi_ai_core = require("rfi-ai-core");
var import_utils = require("rfi-ai-core/utils");
var import_env = require("rfi-ai-shared/env");
var import_img = require("rfi-ai-shared/img");
var import_logger = require("rfi-ai-shared/logger");
var import_utils2 = require("rfi-ai-shared/utils");
var import_webdriverio = require("webdriverio");
var androidScreenshotPath = "/data/local/tmp/midscene_screenshot.png";
var defaultScrollUntilTimes = 10;
var defaultFastScrollDuration = 100;
var defaultNormalScrollDuration = 1e3;
var debugPage = (0, import_logger.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";
(0, import_node_assert.default)(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 (0, import_webdriverio.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 (!(0, import_img.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 = (0, import_utils.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 import_node_fs.default.promises.readFile(screenshotPath);
}
const resizedScreenshotBuffer = await (0, import_img.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: (deltaX, deltaY) => this.mouseWheel(deltaX, deltaY),
move: (x, y) => this.mouseMove(x, y),
drag: (from, to) => this.mouseDrag(from, to)
};
}
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 start = { x: startPoint.left, y: startPoint.top };
const end = { x: start.x, y: 0 };
await this.mouseDrag(start, end);
return;
}
await (0, import_utils2.repeat)(
defaultScrollUntilTimes,
() => this.mouseWheel(0, 9999999, defaultFastScrollDuration)
);
await (0, import_utils.sleep)(1e3);
}
async scrollUntilBottom(startPoint) {
if (startPoint) {
const { height } = await this.size();
const start = { x: startPoint.left, y: startPoint.top };
const end = { x: start.x, y: height };
await this.mouseDrag(start, end);
return;
}
await (0, import_utils2.repeat)(
defaultScrollUntilTimes,
() => this.mouseWheel(0, -9999999, defaultFastScrollDuration)
);
await (0, import_utils.sleep)(1e3);
}
async scrollUntilLeft(startPoint) {
if (startPoint) {
const start = { x: startPoint.left, y: startPoint.top };
const end = { x: 0, y: start.y };
await this.mouseDrag(start, end);
return;
}
await (0, import_utils2.repeat)(
defaultScrollUntilTimes,
() => this.mouseWheel(9999999, 0, defaultFastScrollDuration)
);
await (0, import_utils.sleep)(1e3);
}
async scrollUntilRight(startPoint) {
if (startPoint) {
const { width } = await this.size();
const start = { x: startPoint.left, y: startPoint.top };
const end = { x: width, y: start.y };
await this.mouseDrag(start, end);
return;
}
await (0, import_utils2.repeat)(
defaultScrollUntilTimes,
() => this.mouseWheel(-9999999, 0, defaultFastScrollDuration)
);
await (0, import_utils.sleep)(1e3);
}
async scrollUp(distance, startPoint) {
const { height } = await this.size();
const scrollDistance = distance || height;
if (startPoint) {
const start = { x: startPoint.left, y: startPoint.top };
const endY = Math.max(0, start.y - scrollDistance);
const end = { x: start.x, y: endY };
await this.mouseDrag(start, end);
return;
}
await this.mouseWheel(0, scrollDistance);
}
async scrollDown(distance, startPoint) {
const { height } = await this.size();
const scrollDistance = distance || height;
if (startPoint) {
const start = { x: startPoint.left, y: startPoint.top };
const endY = Math.min(height, start.y + scrollDistance);
const end = { x: start.x, y: endY };
await this.mouseDrag(start, end);
return;
}
await this.mouseWheel(0, -scrollDistance);
}
async scrollLeft(distance, startPoint) {
const { width } = await this.size();
const scrollDistance = distance || width;
if (startPoint) {
const start = { x: startPoint.left, y: startPoint.top };
const endX = Math.max(0, start.x - scrollDistance);
const end = { x: endX, y: start.y };
await this.mouseDrag(start, end);
return;
}
await this.mouseWheel(scrollDistance, 0);
}
async scrollRight(distance, startPoint) {
const { width } = await this.size();
const scrollDistance = distance || width;
if (startPoint) {
const start = { x: startPoint.left, y: startPoint.top };
const endX = Math.min(width, start.x + scrollDistance);
const end = { x: endX, y: start.y };
await this.mouseDrag(start, end);
return;
}
await this.mouseWheel(-scrollDistance, 0);
}
async keyboardType(text, options) {
if (!text)
return;
const driver = await this.getDriver();
const IME_STRATEGY = (this.options?.imeStrategy || (0, import_rfi_ai_core.getAIConfig)(import_env.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.touchAction({
action: "tap",
x: adjustedX,
y: adjustedY
});
}
async mouseMove(x, y) {
return Promise.resolve();
}
async mouseDrag(from, to) {
const driver = await this.getDriver();
const { x: fromX, y: fromY } = this.adjustCoordinates(from.x, from.y);
const { x: toX, y: toY } = this.adjustCoordinates(to.x, to.y);
await driver.touchAction([
{ action: "press", x: fromX, y: fromY },
{ action: "wait", ms: 100 },
{ action: "moveTo", x: toX, y: toY },
{ action: "release" }
]);
}
async mouseWheel(deltaX, deltaY, duration = defaultNormalScrollDuration) {
const { width, height } = await this.size();
const n = 4;
const startX = deltaX < 0 ? (n - 1) * (width / n) : width / n;
const startY = deltaY < 0 ? (n - 1) * (height / n) : height / n;
const maxNegativeDeltaX = startX;
const maxPositiveDeltaX = (n - 1) * (width / n);
const maxNegativeDeltaY = startY;
const maxPositiveDeltaY = (n - 1) * (height / n);
deltaX = Math.max(-maxNegativeDeltaX, Math.min(deltaX, maxPositiveDeltaX));
deltaY = Math.max(-maxNegativeDeltaY, Math.min(deltaY, maxPositiveDeltaY));
const endX = startX + deltaX;
const endY = startY + deltaY;
const { x: adjustedStartX, y: adjustedStartY } = this.adjustCoordinates(
startX,
startY
);
const { x: adjustedEndX, y: adjustedEndY } = this.adjustCoordinates(
endX,
endY
);
const driver = await this.getDriver();
await driver.touchAction([
{ action: "press", x: adjustedStartX, y: adjustedStartY },
{ action: "wait", ms: duration },
{ action: "moveTo", x: adjustedEndX, y: adjustedEndY },
{ action: "release" }
]);
}
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");
}
};
// src/agent/index.ts
var import_env2 = require("rfi-ai-shared/env");
// src/utils/media.ts
var import_node_fs2 = require("fs");
var path = __toESM(require("path"));
var import_logger2 = require("rfi-ai-shared/logger");
var debugMedia = (0, import_logger2.getDebug)("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 (!(0, import_node_fs2.existsSync)(dir)) {
debugMedia(`Creating directory ${dir}`);
(0, import_node_fs2.mkdirSync)(dir, { recursive: true });
}
}
const base64Data = base64Screenshot.replace(/^data:image\/\w+;base64,/, "");
const buffer = Buffer.from(base64Data, "base64");
(0, import_node_fs2.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 (!(0, import_node_fs2.existsSync)(dir)) {
debugMedia(`Creating directory ${dir}`);
(0, import_node_fs2.mkdirSync)(dir, { recursive: true });
}
}
const buffer = Buffer.from(base64Video, "base64");
(0, import_node_fs2.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
var import_logger3 = require("rfi-ai-shared/logger");
var debugDevice = (0, import_logger3.getDebug)("android-device");
var AndroidAgent = class extends import_agent.PageAgent {
constructor(page, opts) {
super(page, opts);
if (!(0, import_env2.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
var import_env3 = require("rfi-ai-shared/env");
// src/performance/index.ts
var import_logger4 = require("rfi-ai-shared/logger");
var debugDevice2 = (0, import_logger4.getDebug)("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
);
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
AndroidAgent,
AndroidDevice,
AndroidDeviceBase,
AppiumDevice,
PerformanceMonitor,
agentFromAdbDevice,
agentFromAppiumServer,
agentFromLocalAppium,
agentFromSauceLabs,
overrideAIConfig,
startVideoRecording,
stopVideoRecording,
takeScreenshot
});