package device import ( "context" "strings" "git.achmad.dev/admin/droidscope/backend/adb" ) type Manager struct { adb *adb.Client Client *adb.Client // exported for use by other packages } func NewManager(client *adb.Client) *Manager { return &Manager{adb: client, Client: client} } // Info fetches static device information for the given device ID. func (m *Manager) Info(ctx context.Context, deviceID string) (*DeviceInfo, error) { return FetchInfo(ctx, m.adb, deviceID) } // LiveStats fetches dynamic device stats for the given device ID. func (m *Manager) LiveStats(ctx context.Context, deviceID string) (*DeviceLiveStats, error) { return FetchLiveStats(ctx, m.adb, deviceID) } // List returns all currently connected devices with their details. func (m *Manager) List(ctx context.Context) ([]Device, error) { raw, err := m.adb.Devices(ctx) if err != nil { return nil, err } serials := parseDeviceLines(raw) devices := make([]Device, 0, len(serials)) for serial, status := range serials { d := Device{ ID: serial, Status: status, ConnectionType: connectionType(serial), } if status == StatusOnline { enrichDevice(ctx, m.adb, &d) } devices = append(devices, d) } return devices, nil } // Connect connects to a wireless ADB device by address (host:port). func (m *Manager) Connect(ctx context.Context, address string) error { out, err := m.adb.Connect(ctx, address) if err != nil { return err } if strings.Contains(out, "failed") || strings.Contains(out, "error") { return &Error{Message: out} } return nil } // Disconnect disconnects a device. func (m *Manager) Disconnect(ctx context.Context, deviceID string) error { _, err := m.adb.Disconnect(ctx, deviceID) return err } // parseDeviceLines parses `adb devices -l` output into a serial→status map. func parseDeviceLines(raw string) map[string]Status { result := make(map[string]Status) for _, line := range strings.Split(raw, "\n") { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "List of") { continue } fields := strings.Fields(line) if len(fields) < 2 { continue } serial := fields[0] result[serial] = parseStatus(fields[1]) } return result } func parseStatus(s string) Status { switch s { case "device": return StatusOnline case "offline": return StatusOffline case "unauthorized": return StatusUnauthorized case "recovery": return StatusRecovery default: return StatusOffline } } func connectionType(serial string) ConnectionType { if strings.Contains(serial, ":") { return ConnectionWifi } return ConnectionUSB } // enrichDevice fetches device properties via adb shell getprop. func enrichDevice(ctx context.Context, client *adb.Client, d *Device) { props := map[string]*string{ "ro.product.model": &d.Model, "ro.product.manufacturer": &d.Manufacturer, "ro.build.version.release": &d.AndroidVersion, "ro.product.cpu.abi": &d.ABI, "ro.product.name": &d.Product, } for prop, dest := range props { if val, err := client.Shell(ctx, d.ID, "getprop "+prop); err == nil { *dest = strings.TrimSpace(val) } } if level, err := client.Shell(ctx, d.ID, "dumpsys battery | grep level | tr -dc '0-9'"); err == nil { d.BatteryLevel = parseIntSafe(strings.TrimSpace(level)) } } func parseIntSafe(s string) int { n := 0 for _, c := range s { if c >= '0' && c <= '9' { n = n*10 + int(c-'0') } } return n } type Error struct { Message string } func (e *Error) Error() string { return e.Message }