Skip to main content
Glama
android.go11.1 kB
package device import ( "bytes" "fmt" "log" "os/exec" "strconv" "strings" "github.com/pkg/errors" ) const ( gboxRegIdSettingKey = "gbox_reg_id" gboxDeviceIDFileDir = "/sdcard/.gbox" gboxRegIdFilePath = "/sdcard/.gbox/reg_id" ) // AndroidManager manages Android devices (implements DeviceManager) type AndroidManager struct { adbPath string } // GetDevices returns list of connected Android devices func (m *AndroidManager) GetDevices() ([]DeviceInfo, error) { cmd := exec.Command(m.adbPath, "devices", "-l") output, err := cmd.Output() if err != nil { return nil, fmt.Errorf("failed to run adb devices: %w", err) } lines := strings.Split(string(output), "\n") var devices []DeviceInfo for _, line := range lines { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "List of devices") { continue } parts := strings.Fields(line) if len(parts) < 2 { continue } deviceID := parts[0] status := parts[1] // Only include devices with "device" status if status != "device" { continue } device := DeviceInfo{ ID: deviceID, Status: status, ConnectionType: "usb", // Default to USB connection IsRegistrable: false, // Default to false, will be updated by caller if needed } // Check if device is connected via network if strings.Contains(deviceID, "._adb._tcp") { // mDNS service name (e.g., "adb-A4RYVB3A20008848._adb._tcp") device.ConnectionType = "mdns" // Keep the full mDNS name as device ID } else if strings.Contains(deviceID, ":") { // IP address with port (e.g., "192.168.1.100:5555") device.ConnectionType = "ip" } // Parse additional device info if available if len(parts) > 2 { for _, part := range parts[2:] { if strings.Contains(part, ":") { kv := strings.SplitN(part, ":", 2) if len(kv) == 2 { // Map common fields to expected names switch kv[0] { case "model": device.Model = kv[1] case "device": device.Manufacturer = kv[1] } } } } } // Get serial number and Android ID serialNo, err := m.getSerialNo(deviceID) if err != nil { log.Printf("Failed to get serialno of device %s: %v", deviceID, err) // Use device ID as fallback for serial number device.SerialNo = deviceID device.AndroidID = "" } else { device.SerialNo = serialNo androidID, err := m.getAndroidID(deviceID) if err != nil { log.Printf("Failed to get android id of device %s: %v", deviceID, err) device.AndroidID = "" } else { device.AndroidID = androidID } } // Get reg_id for this device (non-fatal if fails) regId, _ := m.GetRegId(deviceID) device.RegId = regId devices = append(devices, device) } return devices, nil } // getSerialNo gets the device serial number func (m *AndroidManager) getSerialNo(deviceID string) (string, error) { cmd := exec.Command(m.adbPath, "-s", deviceID, "shell", "getprop", "ro.serialno") output, err := cmd.Output() if err != nil { return "", errors.Wrapf(err, "failed to get serialno of device %s", deviceID) } return strings.TrimSpace(string(output)), nil } // getAndroidID gets the device Android ID func (m *AndroidManager) getAndroidID(deviceID string) (string, error) { cmd := exec.Command(m.adbPath, "-s", deviceID, "shell", "settings", "get", "secure", "android_id") output, err := cmd.Output() if err != nil { return "", errors.Wrapf(err, "failed to get android id of device %s", deviceID) } return strings.TrimSpace(string(output)), nil } // GetIdentifiers returns device identifiers for the given device. func (m *AndroidManager) GetIdentifiers(deviceID string) (Identifiers, error) { serialNo, err := m.getSerialNo(deviceID) if err != nil { return Identifiers{}, err } androidID, err := m.getAndroidID(deviceID) if err != nil { return Identifiers{}, err } regId, _ := m.GetRegId(deviceID) // non-fatal return Identifiers{ SerialNo: serialNo, AndroidID: &androidID, // Use pointer for Android devices RegId: regId, }, nil } // SetRegId writes a registration ID to the device. // It first tries to write into Android settings (global). If that fails (e.g., permission denied), // it falls back to writing a file on external storage. func (m *AndroidManager) SetRegId(deviceID string, regId string) error { // Try settings put global first putCmd := exec.Command(m.adbPath, "-s", deviceID, "shell", "settings", "put", "global", gboxRegIdSettingKey, regId) if err := putCmd.Run(); err == nil { // Verify from settings getCmd := exec.Command(m.adbPath, "-s", deviceID, "shell", "settings", "get", "global", gboxRegIdSettingKey) out, verr := getCmd.Output() if verr == nil { got := strings.TrimSpace(string(out)) if got != "" && got != "null" && got == strings.TrimSpace(regId) { // Enforce single source of truth: delete file; if deletion fails, report error rmCmd := exec.Command(m.adbPath, "-s", deviceID, "shell", "rm", "-f", gboxRegIdFilePath) if err := rmCmd.Run(); err != nil { return errors.Wrap(err, "failed to delete fallback reg_id file after successful settings write") } return nil } } // if verification failed, fall through to file fallback } // Fallback: write to file only (do not attempt settings again) shell := fmt.Sprintf("mkdir -p %s && printf %s %s > %s", gboxDeviceIDFileDir, "%s", shellQuoteForSingle(regId), gboxRegIdFilePath) fileCmd := exec.Command(m.adbPath, "-s", deviceID, "shell", "sh", "-c", shell) if err := fileCmd.Run(); err != nil { return errors.Wrap(err, "failed to write reg id to file") } // Verify by reading the file readCmd := exec.Command(m.adbPath, "-s", deviceID, "shell", "cat", gboxRegIdFilePath) out, err := readCmd.Output() if err != nil { return errors.Wrap(err, "failed to read back reg id from file") } got := strings.TrimSpace(string(out)) if got != strings.TrimSpace(regId) { return fmt.Errorf("verification failed (file): expected %q, got %q", regId, got) } return nil } // GetRegId reads the registration ID from settings or fallback file. func (m *AndroidManager) GetRegId(deviceID string) (string, error) { // Prefer file first readCmd := exec.Command(m.adbPath, "-s", deviceID, "shell", "cat", gboxRegIdFilePath) out, err := readCmd.Output() if err == nil { v := strings.TrimSpace(string(out)) if v != "" { return v, nil } } // Then try settings getCmd := exec.Command(m.adbPath, "-s", deviceID, "shell", "settings", "get", "global", gboxRegIdSettingKey) out, err = getCmd.Output() if err != nil { return "", errors.Wrap(err, "failed to read reg id from settings") } v := strings.TrimSpace(string(out)) if v == "null" { v = "" } return v, nil } // shellQuoteForSingle returns a single-quoted shell-safe string, handling embedded single quotes. // e.g., abc'def -> 'abc'"'"'def' func shellQuoteForSingle(s string) string { if s == "" { return "''" } return "'" + strings.ReplaceAll(s, "'", "'\"'\"'") + "'" } type AdbCommandResult struct { Stdout string `json:"stdout"` Stderr string `json:"stderr"` ExitCode int `json:"exitCode"` } func (m *AndroidManager) ExecAdbCommand(deviceID, command string) (*AdbCommandResult, error) { cmd := exec.Command("sh", "-c", strings.Join([]string{m.adbPath, "-s", deviceID, command}, " ")) var stdoutBuf, stderrBuf bytes.Buffer cmd.Stdout = &stdoutBuf cmd.Stderr = &stderrBuf if err := cmd.Run(); err != nil { if exitError, ok := err.(*exec.ExitError); ok { return &AdbCommandResult{ Stdout: stdoutBuf.String(), Stderr: stderrBuf.String(), ExitCode: exitError.ExitCode(), }, nil } return nil, errors.Wrapf(err, "failed to exec adb command on device %s", deviceID) } return &AdbCommandResult{ Stdout: stdoutBuf.String(), Stderr: stderrBuf.String(), ExitCode: 0, }, nil } // GetDisplayResolution returns the device display resolution (width, height) in pixels. // It prefers the "Override size" reported by `wm size` when present; otherwise it // falls back to the "Physical size". func (m *AndroidManager) GetDisplayResolution(deviceID string) (int, int, error) { cmd := exec.Command(m.adbPath, "-s", deviceID, "shell", "wm", "size") output, err := cmd.Output() if err != nil { return 0, 0, errors.Wrapf(err, "failed to run wm size for device %s", deviceID) } stdout := strings.TrimSpace(string(output)) var sizeLine string lines := strings.Split(stdout, "\n") // Prefer Override size; otherwise use Physical size for _, line := range lines { trimmed := strings.TrimSpace(line) if strings.HasPrefix(trimmed, "Override size:") { sizeLine = strings.TrimSpace(strings.TrimPrefix(trimmed, "Override size:")) break } if strings.HasPrefix(trimmed, "Physical size:") { if sizeLine == "" { sizeLine = strings.TrimSpace(strings.TrimPrefix(trimmed, "Physical size:")) } } } if sizeLine == "" { return 0, 0, fmt.Errorf("invalid screen size output: %s", stdout) } parts := strings.Split(sizeLine, "x") if len(parts) != 2 { return 0, 0, fmt.Errorf("invalid screen size output: %s", stdout) } width, err := strconv.Atoi(strings.TrimSpace(parts[0])) if err != nil { return 0, 0, fmt.Errorf("invalid screen size dimensions: %s", sizeLine) } height, err := strconv.Atoi(strings.TrimSpace(parts[1])) if err != nil { return 0, 0, fmt.Errorf("invalid screen size dimensions: %s", sizeLine) } return width, height, nil } // GetOSVersion returns the Android OS version (e.g., "14", "13") func (m *AndroidManager) GetOSVersion(deviceID string) (string, error) { // Try ro.build.version.release first (user-friendly version like "14", "13") cmd := exec.Command(m.adbPath, "-s", deviceID, "shell", "getprop", "ro.build.version.release") output, err := cmd.Output() if err == nil { version := strings.TrimSpace(string(output)) if version != "" { return version, nil } } // Fallback to SDK version cmd = exec.Command(m.adbPath, "-s", deviceID, "shell", "getprop", "ro.build.version.sdk") output, err = cmd.Output() if err == nil { version := strings.TrimSpace(string(output)) if version != "" { return version, nil } } return "", fmt.Errorf("failed to get Android OS version") } // GetMemory returns the total memory in GB (e.g., "8 GB") func (m *AndroidManager) GetMemory(deviceID string) (string, error) { // Read MemTotal from /proc/meminfo cmd := exec.Command(m.adbPath, "-s", deviceID, "shell", "cat", "/proc/meminfo") output, err := cmd.Output() if err != nil { return "", errors.Wrapf(err, "failed to read meminfo for device %s", deviceID) } lines := strings.Split(string(output), "\n") for _, line := range lines { if strings.HasPrefix(line, "MemTotal:") { // Parse line like "MemTotal: 16384000 kB" fields := strings.Fields(line) if len(fields) >= 2 { memKB, err := strconv.ParseInt(fields[1], 10, 64) if err == nil { // Convert KB to GB memGB := float64(memKB) / (1024 * 1024) return fmt.Sprintf("%.0f GB", memGB), nil } } } } return "", fmt.Errorf("failed to parse memory information") }

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/babelcloud/gru-sandbox'

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