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.
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user