Skip to main content
Glama
deviceSessionManager.ts12 kB
import { ActionableError, BootedDevice, Platform, SomePlatform } from "../models"; import { DeviceUtils } from "./deviceUtils"; import { AdbUtils } from "./android-cmdline-tools/adb"; import { Simctl } from "./ios-cmdline-tools/simctl"; import { Window } from "../features/observe/Window"; import { logger } from "./logger"; import { AccessibilityServiceManager } from "./accessibilityServiceManager"; import { AndroidEmulator } from "./android-cmdline-tools/emulator"; export class DeviceSessionManager { private currentDevice: BootedDevice | undefined; private currentPlatform: Platform | undefined; private static instance: DeviceSessionManager; private adb: AdbUtils; private simctl: Simctl; private androidEmulator: AndroidEmulator; private deviceUtils: DeviceUtils; private window: Window | undefined; private constructor() { this.adb = new AdbUtils(null); this.simctl = new Simctl(null); this.androidEmulator = new AndroidEmulator(); this.deviceUtils = new DeviceUtils( this.adb, this.simctl, this.androidEmulator ); } public static getInstance(): DeviceSessionManager { if (!DeviceSessionManager.instance) { DeviceSessionManager.instance = new DeviceSessionManager(); } return DeviceSessionManager.instance; } /** * Get the current device ID */ public getCurrentDevice(): BootedDevice | undefined { return this.currentDevice; } /** * Get the current platform */ public getCurrentPlatform(): Platform | undefined { return this.currentPlatform; } /** * Set the current device ID and platform */ public setCurrentDevice(device: BootedDevice, platform: Platform): void { this.currentDevice = device; this.currentPlatform = platform; if (platform === "android") { // Update AdbUtils with new device ID this.adb = new AdbUtils(device); } // Reset window when device changes this.window = undefined; } /** * Detect the platform of connected devices */ private async detectConnectedPlatforms(): Promise<BootedDevice[]> { const devices: BootedDevice[] = []; try { // Check for Android devices via ADB const androidDevices = await this.adb.getBootedAndroidDevices(); devices.push(...androidDevices); } catch (error) { logger.warn(`Failed to detect Android devices: ${error}`); } try { // Check for iOS devices/simulators via xcrun simctl const iosDevices = await this.simctl.getBootedSimulators(); devices.push(...iosDevices); } catch (error) { logger.warn(`Failed to detect iOS devices: ${error}`); } return devices; } /** * Ensure a device is ready for the specified platform and return its ID * Throws an error if both Android and iOS devices are connected */ public async ensureDeviceReady(platform: SomePlatform, providedDeviceId?: string): Promise<BootedDevice> { // Detect all connected devices const connectedPlatforms = await this.detectConnectedPlatforms(); logger.info(`Found ${connectedPlatforms.length} connectedPlatform devices`); const androidDevices = connectedPlatforms.filter(device => device.platform === "android"); logger.info(`Found ${androidDevices.length} android devices`); const iosDevices = connectedPlatforms.filter(device => device.platform === "ios"); logger.info(`Found ${iosDevices.length} ios devices`); // Check if both platforms have devices - this is not allowed if (androidDevices.length > 0 && iosDevices.length > 0) { throw new ActionableError( "Both Android and iOS devices are connected. Please disconnect devices from one platform to continue." ); } // Get devices for the requested platform let platformDevices: BootedDevice[] = []; let resolvedPlatform: Platform; switch (platform) { case "android": platformDevices = androidDevices; resolvedPlatform = "android"; break; case "ios": platformDevices = iosDevices; resolvedPlatform = "ios"; break; default: if (androidDevices.length > 0) { platformDevices = androidDevices; resolvedPlatform = "android"; } else if (iosDevices.length > 0) { platformDevices = iosDevices; resolvedPlatform = "ios"; } else { platformDevices = []; resolvedPlatform = "android"; } } // If a specific device is provided, verify it exists on the correct platform if (providedDeviceId && resolvedPlatform) { const providedDevice = platformDevices.find(device => device.deviceId === providedDeviceId); if (!providedDevice) { throw new ActionableError( `Device ${providedDeviceId} not found on ${platform} platform. ` + `Available ${platform} devices: ${platformDevices.join(", ") || "none"}` ); } await this.verifyDevice(providedDeviceId, resolvedPlatform); this.setCurrentDevice(providedDevice, resolvedPlatform); return providedDevice; } // If we have a current device for the requested platform, verify it's still ready if (this.currentDevice && this.currentPlatform === platform) { try { await this.verifyDevice(this.currentDevice.deviceId, platform); return this.currentDevice; } catch (error) { logger.warn(`Current device ${this.currentDevice} is no longer ready: ${error}`); this.currentDevice = undefined; this.currentPlatform = undefined; } } // No device set - find or start one for the requested platform const device = await this.findOrStartDevice(resolvedPlatform); this.setCurrentDevice(device, resolvedPlatform); return device; } /** * Verify a specific device is connected and ready for the given platform */ private async verifyDevice(deviceId: string, platform: Platform): Promise<void> { if (platform === "android") { await this.verifyAndroidDevice(deviceId); } else { await this.verifyIosDevice(deviceId); } } /** * Verify an Android device is connected and ready */ private async verifyAndroidDevice(deviceId: string): Promise<void> { const allDevices = await this.adb.getBootedAndroidDevices(); const device = allDevices.find(device => device.name === deviceId); if (!device) { throw new ActionableError( `Android device ${deviceId} is not connected. Available devices: ${allDevices.join(", ") || "none"}` ); } // Check if we can get an active window from the device try { logger.info(`[DeviceSessionManager] Verifying Android device ${deviceId} readiness`); if (!this.window || this.window.getDeviceId() !== deviceId) { this.window = new Window(device); } let activeWindow = await this.window.getActive(); if (!activeWindow || !activeWindow.appId || !activeWindow.activityName) { activeWindow = await this.window.getActive(true); if (!activeWindow || !activeWindow.appId || !activeWindow.activityName) { logger.warn(`[DeviceSessionManager] Android device ${deviceId} is not fully ready`); if (activeWindow) { logger.warn(`[DeviceSessionManager] activeWindow.appId: ${activeWindow.appId} | activeWindow.activityName: ${activeWindow.activityName}`); } else { logger.warn(`[DeviceSessionManager] activeWindow: ${activeWindow}`); } throw new ActionableError( `Cannot get active window information from Android device ${deviceId}. The device may not be fully booted or is in an unusual state.` ); } } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new ActionableError( `Failed to verify Android device ${deviceId} readiness: ${errorMessage}` ); } try { await AccessibilityServiceManager.getInstance(device).setup(); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error(`[DeviceSessionManager] Failed to setup accessibility service: ${errorMessage}`); // Rethrow ActionableErrors to preserve their specific error messages if (error instanceof ActionableError) { throw error; } } } /** * Verify an iOS device is connected and ready */ private async verifyIosDevice(deviceId: string): Promise<void> { const deviceInfo = await this.simctl.getDeviceInfo(deviceId); if (!deviceInfo) { throw new ActionableError( `iOS simulator ${deviceId} is not available. Please check if it exists and is available.` ); } if (!deviceInfo.isAvailable) { throw new ActionableError( `iOS simulator ${deviceId} is not available (state: ${deviceInfo.state}). Please check simulator availability.` ); } // If simulator is not booted, we could boot it, but for now we'll just check if (deviceInfo.state !== "Booted") { logger.info(`iOS simulator ${deviceId} is not booted (state: ${deviceInfo.state})`); // Note: We could auto-boot here if desired, but keeping consistent with current behavior } } /** * Find an available device or start an emulator for the specified platform */ private async findOrStartDevice(platform: Platform): Promise<BootedDevice> { if (platform === "android") { return await this.findOrStartAndroidDevice(); } else { return await this.findOrStartIosDevice(); } } /** * Find an available Android device or start an emulator */ private async findOrStartAndroidDevice(): Promise<BootedDevice> { const allDevices = await this.deviceUtils.getBootedDevices("android"); if (allDevices.length > 0) { // Use the first available device const device = allDevices[0]; const deviceId = device.deviceId!; await this.verifyAndroidDevice(deviceId); return device; } // No devices - try to start a device from an image const availableImages = await this.deviceUtils.listDeviceImages("android"); if (availableImages.length === 0) { throw new ActionableError( "No devices are connected and no device images are available. Please connect a physical device or create a device image first." ); } // Start the first available AVD const deviceImage = availableImages[0]; logger.info(`Starting Android emulator ${deviceImage}...`); await this.deviceUtils.startDevice(deviceImage); // Wait for the emulator to fully boot and get its device ID const newDevice = await this.deviceUtils.waitForDeviceReady(deviceImage); if (!newDevice) { throw new ActionableError( `Failed to start Android emulator ${deviceImage}.` ); } await this.verifyAndroidDevice(newDevice.deviceId!); return newDevice; } /** * Find an available iOS device or start a simulator */ private async findOrStartIosDevice(): Promise<BootedDevice> { const allDevices = await this.simctl.listSimulatorImages(); if (allDevices.length === 0) { throw new ActionableError( "No iOS simulators are available. Please create an iOS simulator using Xcode or the Simulator app." ); } // Check for already booted simulators first const bootedDevices = await this.simctl.getBootedSimulators(); if (bootedDevices.length > 0) { // Use the first booted device const device = bootedDevices[0]; await this.verifyIosDevice(device.deviceId!); return device; } // No booted devices - boot the first available simulator const device = allDevices[0]; const deviceId = device.deviceId!; logger.info(`Booting iOS simulator ${device}...`); const bootedDevice = await this.simctl.bootSimulator(deviceId); await this.verifyIosDevice(deviceId); return bootedDevice; } }

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/zillow/auto-mobile'

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