Skip to main content
Glama
adbClient.ts5.39 kB
import adb, { StartActivityOptions } from '@devicefarmer/adbkit'; import { XMLParser } from 'fast-xml-parser'; export type UiNode = { text?: string; contentDesc?: string; resourceId?: string; class?: string; bounds?: string; children?: UiNode[]; }; type AdbDevice = { id: string; type: string }; export class AdbService { // Using any to stay compatible with adbkit's CJS export shape. private client: any; private defaultSerial?: string; constructor() { const adbAny = adb as any; const createClient = adbAny.createClient ?? adbAny.default?.createClient; if (typeof createClient !== 'function') { throw new Error('adbkit createClient not found'); } this.client = createClient(); } async listDevices(): Promise<AdbDevice[]> { return this.client.listDevices(); } setDefaultDevice(serial: string | undefined): void { this.defaultSerial = serial; } private async resolveSerial(serial?: string): Promise<string> { const target = serial ?? this.defaultSerial; if (target) return target; const devices = await this.listDevices(); if (devices.length === 1) { const single = devices[0]; this.defaultSerial = single.id; return single.id; } throw new Error('No device selected. Provide a serial or call selectDevice when multiple devices are attached.'); } async takeScreenshot(serial?: string): Promise<Buffer> { const { deviceClient } = await this.getDeviceClient(serial); try { const stream = await deviceClient.screencap(); return await this.readStreamAsBuffer(stream); } catch { // Fallback to shell-based screencap for devices that lack the service const stream = await deviceClient.shell('screencap -p'); return await this.readStreamAsBuffer(stream); } } async sendKeyEvent(keyCode: number, serial?: string): Promise<void> { const { deviceClient } = await this.getDeviceClient(serial); await deviceClient.shell(`input keyevent ${keyCode}`); } async tap(x: number, y: number, serial?: string): Promise<void> { const { deviceClient } = await this.getDeviceClient(serial); await deviceClient.shell(`input tap ${x} ${y}`); } async inputText(text: string, serial?: string): Promise<void> { const { deviceClient } = await this.getDeviceClient(serial); const escaped = text.replace(/ /g, '%s'); await deviceClient.shell(`input text "${escaped}"`); } async startActivity( options: Omit<StartActivityOptions, 'component'> & { component: string }, serial?: string, ): Promise<void> { const { deviceClient } = await this.getDeviceClient(serial); await deviceClient.startActivity(options); } async uiDump(serial?: string): Promise<UiNode> { const { deviceClient } = await this.getDeviceClient(serial); const dumpCommand = 'uiautomator dump /sdcard/uidump.xml && cat /sdcard/uidump.xml && rm /sdcard/uidump.xml'; const stream = await deviceClient.shell(dumpCommand); const xml = await this.readStreamAsString(stream); const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '', parseAttributeValue: false, }); const parsed = parser.parse(xml); return this.normalizeUiTree(parsed?.hierarchy ?? parsed); } async getDeviceInfo(serial?: string): Promise<{ serial: string; model: string; manufacturer: string; sdk: string; device: string; product: string; }> { const { deviceClient, resolved } = await this.getDeviceClient(serial); const props = [ 'ro.product.model', 'ro.product.manufacturer', 'ro.build.version.sdk', 'ro.product.device', 'ro.product.name', ]; const cmd = props.map((p) => `getprop ${p}`).join(' && '); const stream = await deviceClient.shell(cmd); const out = (await this.readStreamAsString(stream)) .split('\n') .map((s) => s.trim()) .filter(Boolean); const [model, manufacturer, sdk, device, product] = out; return { serial: resolved, model: model ?? '', manufacturer: manufacturer ?? '', sdk: sdk ?? '', device: device ?? '', product: product ?? '', }; } private normalizeUiTree(node: any): UiNode { if (!node) { return {}; } const childrenArray = Array.isArray(node.node) ? node.node : node.node ? [node.node] : []; return { text: node.text, contentDesc: node['content-desc'], resourceId: node['resource-id'], class: node.class, bounds: node.bounds, children: childrenArray.map((child: any) => this.normalizeUiTree(child)), }; } private async readStreamAsString(stream: NodeJS.ReadableStream): Promise<string> { const buffer = await this.readStreamAsBuffer(stream); return buffer.toString('utf8'); } private async readStreamAsBuffer(stream: NodeJS.ReadableStream): Promise<Buffer> { const chunks: Buffer[] = []; return new Promise((resolve, reject) => { stream.on('data', (d: Buffer) => chunks.push(d)); stream.on('end', () => resolve(Buffer.concat(chunks))); stream.on('error', reject); }); } private async getDeviceClient(serial?: string) { const resolved = await this.resolveSerial(serial); const deviceClient = this.client.getDevice(resolved); return { deviceClient, resolved }; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/hamen/android-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server