Files
DroidScope/backend-go/device/manager.go
T
achmad 14935db63e feat: implement ADB wrapper and device management backend
Adds full ADB client, device manager, static DeviceInfo fetcher, and
live DeviceLiveStats poller. Exposes ListDevices, ConnectDevice,
DisconnectDevice, GetDeviceInfo, GetDeviceLiveStats as Wails-bound
methods with a 1s devices:changed event loop. Bundles ADB binary
infrastructure via //go:embed all:bin with runtime fallback chain.
2026-05-06 14:51:34 +07:00

143 lines
3.5 KiB
Go

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 }