Files
DroidScope/backend-go/device/info.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

144 lines
3.8 KiB
Go

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"
}
}