package device import ( "context" "fmt" "strings" "git.achmad.dev/admin/droidscope/backend/adb" ) // FetchInfo retrieves static device information (fetched once on device select). func FetchInfo(ctx context.Context, client *adb.Client, deviceID string) (*DeviceInfo, error) { info := &DeviceInfo{ID: deviceID} shell := func(cmd string) string { out, _ := client.Shell(ctx, deviceID, cmd) return strings.TrimSpace(out) } info.Manufacturer = shell("getprop ro.product.manufacturer") info.Model = shell("getprop ro.product.model") info.Product = shell("getprop ro.product.name") info.AndroidVersion = shell("getprop ro.build.version.release") info.SDKVersion = shell("getprop ro.build.version.sdk") info.SecurityPatch = shell("getprop ro.build.version.security_patch") info.BuildFingerprint = shell("getprop ro.build.fingerprint") info.ABI = shell("getprop ro.product.cpu.abi") info.SupportedABIs = shell("getprop ro.product.cpu.abilist") info.SerialNumber = shell("getprop ro.serialno") info.ScreenResolution = shell("wm size | awk '/Physical/{print $3}'") info.ScreenDensity = shell("wm density | awk '/Physical/{print $3}' | tr -d '\\n'") info.TotalRAMMB = parseRAM(shell("cat /proc/meminfo | grep MemTotal")) info.TotalStorageGB, _ = parseStorage(shell("df /data 2>/dev/null | tail -1")) return info, nil } // FetchLiveStats retrieves only the fields that change at runtime. // Called on a 1-second poll interval. func FetchLiveStats(ctx context.Context, client *adb.Client, deviceID string) (*DeviceLiveStats, error) { shell := func(cmd string) string { out, _ := client.Shell(ctx, deviceID, cmd) return strings.TrimSpace(out) } stats := &DeviceLiveStats{} stats.AvailableRAMMB = parseRAM(shell("cat /proc/meminfo | grep MemAvailable")) _, stats.AvailableStorageGB = parseStorage(shell("df /data 2>/dev/null | tail -1")) batteryOut := shell("dumpsys battery") stats.BatteryLevel = parseBatteryField(batteryOut, "level") stats.BatteryStatus = parseBatteryStatusCode(parseBatteryField(batteryOut, "status")) stats.ThermalStatus = parseThermalStatus(shell("dumpsys thermalservice 2>/dev/null | grep 'Current thermal status'")) ip := shell("ip addr show wlan0 2>/dev/null | grep 'inet ' | awk '{print $2}' | cut -d/ -f1") if ip == "" { ip = shell("ip route get 8.8.8.8 2>/dev/null | grep src | awk '{print $7}'") } stats.IPAddress = ip return stats, nil } func parseRAM(line string) int { fields := strings.Fields(line) if len(fields) < 2 { return 0 } return parseIntSafe(fields[1]) / 1024 } func parseStorage(dfLine string) (total, available string) { fields := strings.Fields(dfLine) if len(fields) < 4 { return "", "" } return formatGB(parseIntSafe(fields[1])), formatGB(parseIntSafe(fields[3])) } func formatGB(kb int) string { if kb == 0 { return "" } gb := float64(kb) / 1024 / 1024 if gb >= 1 { return strings.TrimRight(strings.TrimRight( strings.Replace(fmt.Sprintf("%.1f", gb), ".0", "", 1), "0"), ".") + " GB" } return fmt.Sprintf("%d MB", kb/1024) } func parseBatteryField(dumpsys, field string) int { for _, line := range strings.Split(dumpsys, "\n") { line = strings.TrimSpace(line) if strings.HasPrefix(line, field+":") { return parseIntSafe(strings.TrimSpace(strings.TrimPrefix(line, field+":"))) } } return 0 } func parseBatteryStatusCode(code int) string { switch code { case 2: return "charging" case 3: return "discharging" case 4: return "not charging" case 5: return "full" default: return "unknown" } } func parseThermalStatus(line string) string { parts := strings.Split(line, ":") if len(parts) < 2 { return "none" } switch parseIntSafe(strings.TrimSpace(parts[1])) { case 1: return "light" case 2: return "moderate" case 3: return "severe" case 4: return "critical" case 5: return "emergency" case 6: return "shutdown" default: return "none" } }