14935db63e
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.
144 lines
3.8 KiB
Go
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"
|
|
}
|
|
}
|