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:
@@ -1 +1,81 @@
|
|||||||
package adb
|
package adb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(adbPath string) *Client {
|
||||||
|
return &Client{path: adbPath}
|
||||||
|
}
|
||||||
|
|
||||||
|
// run executes an adb command and returns stdout.
|
||||||
|
func (c *Client) run(ctx context.Context, args ...string) (string, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, c.path, args...)
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
msg := strings.TrimSpace(stderr.String())
|
||||||
|
if msg == "" {
|
||||||
|
msg = err.Error()
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("adb %s: %s", strings.Join(args, " "), msg)
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(stdout.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Devices returns raw output of `adb devices -l`.
|
||||||
|
func (c *Client) Devices(ctx context.Context) (string, error) {
|
||||||
|
return c.run(ctx, "devices", "-l")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shell runs a shell command on a specific device and returns stdout.
|
||||||
|
func (c *Client) Shell(ctx context.Context, deviceID, cmd string) (string, error) {
|
||||||
|
return c.run(ctx, "-s", deviceID, "shell", cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect connects to a device over TCP/IP (wireless ADB).
|
||||||
|
func (c *Client) Connect(ctx context.Context, address string) (string, error) {
|
||||||
|
return c.run(ctx, "connect", address)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disconnect disconnects a device.
|
||||||
|
func (c *Client) Disconnect(ctx context.Context, deviceID string) (string, error) {
|
||||||
|
return c.run(ctx, "disconnect", deviceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull copies a file from the device to a local path.
|
||||||
|
func (c *Client) Pull(ctx context.Context, deviceID, remotePath, localPath string) error {
|
||||||
|
_, err := c.run(ctx, "-s", deviceID, "pull", remotePath, localPath)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push copies a local file to the device.
|
||||||
|
func (c *Client) Push(ctx context.Context, deviceID, localPath, remotePath string) error {
|
||||||
|
_, err := c.run(ctx, "-s", deviceID, "push", localPath, remotePath)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartServer starts the ADB server if not already running.
|
||||||
|
func (c *Client) StartServer(ctx context.Context) error {
|
||||||
|
_, err := c.run(ctx, "start-server")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path returns the resolved ADB binary path.
|
||||||
|
func (c *Client) Path() string {
|
||||||
|
return c.path
|
||||||
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,142 @@
|
|||||||
package device
|
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 }
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package device
|
||||||
|
|
||||||
|
type ConnectionType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ConnectionUSB ConnectionType = "usb"
|
||||||
|
ConnectionWifi ConnectionType = "wifi"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Status string
|
||||||
|
|
||||||
|
const (
|
||||||
|
StatusOnline Status = "online"
|
||||||
|
StatusOffline Status = "offline"
|
||||||
|
StatusUnauthorized Status = "unauthorized"
|
||||||
|
StatusRecovery Status = "recovery"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Device struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
Product string `json:"product"`
|
||||||
|
AndroidVersion string `json:"androidVersion"`
|
||||||
|
ABI string `json:"abi"`
|
||||||
|
ConnectionType ConnectionType `json:"connectionType"`
|
||||||
|
Status Status `json:"status"`
|
||||||
|
BatteryLevel int `json:"batteryLevel"`
|
||||||
|
Manufacturer string `json:"manufacturer"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeviceLiveStats holds fields that change while the device is running.
|
||||||
|
// Polled separately from the static DeviceInfo.
|
||||||
|
type DeviceLiveStats struct {
|
||||||
|
AvailableRAMMB int `json:"availableRamMb"`
|
||||||
|
BatteryLevel int `json:"batteryLevel"`
|
||||||
|
BatteryStatus string `json:"batteryStatus"`
|
||||||
|
ThermalStatus string `json:"thermalStatus"`
|
||||||
|
AvailableStorageGB string `json:"availableStorageGb"`
|
||||||
|
IPAddress string `json:"ipAddress"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeviceInfo holds extended device details fetched on demand.
|
||||||
|
type DeviceInfo struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Manufacturer string `json:"manufacturer"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
Product string `json:"product"`
|
||||||
|
AndroidVersion string `json:"androidVersion"`
|
||||||
|
SDKVersion string `json:"sdkVersion"`
|
||||||
|
SecurityPatch string `json:"securityPatch"`
|
||||||
|
BuildFingerprint string `json:"buildFingerprint"`
|
||||||
|
ABI string `json:"abi"`
|
||||||
|
SupportedABIs string `json:"supportedAbis"`
|
||||||
|
ScreenResolution string `json:"screenResolution"`
|
||||||
|
ScreenDensity string `json:"screenDensity"`
|
||||||
|
TotalRAMMB int `json:"totalRamMb"`
|
||||||
|
AvailableRAMMB int `json:"availableRamMb"`
|
||||||
|
TotalStorageGB string `json:"totalStorageGb"`
|
||||||
|
AvailableStorageGB string `json:"availableStorageGb"`
|
||||||
|
BatteryLevel int `json:"batteryLevel"`
|
||||||
|
BatteryStatus string `json:"batteryStatus"`
|
||||||
|
ThermalStatus string `json:"thermalStatus"`
|
||||||
|
IPAddress string `json:"ipAddress"`
|
||||||
|
SerialNumber string `json:"serialNumber"`
|
||||||
|
}
|
||||||
+80
-9
@@ -2,26 +2,97 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"time"
|
||||||
|
|
||||||
|
"git.achmad.dev/admin/droidscope/backend/adb"
|
||||||
|
"git.achmad.dev/admin/droidscope/backend/device"
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
|
|
||||||
|
"git.achmad.dev/admin/droidscope/desktop/internal/adbembed"
|
||||||
)
|
)
|
||||||
|
|
||||||
// App struct
|
|
||||||
type App struct {
|
type App struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
deviceManager *device.Manager
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewApp creates a new App application struct
|
|
||||||
func NewApp() *App {
|
func NewApp() *App {
|
||||||
return &App{}
|
return &App{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// startup is called when the app starts. The context is saved
|
|
||||||
// so we can call the runtime methods
|
|
||||||
func (a *App) startup(ctx context.Context) {
|
func (a *App) startup(ctx context.Context) {
|
||||||
a.ctx = ctx
|
a.ctx = ctx
|
||||||
|
|
||||||
|
adbPath, err := adbembed.Resolve()
|
||||||
|
if err != nil {
|
||||||
|
runtime.LogErrorf(ctx, "ADB not found: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
runtime.LogInfof(ctx, "Using ADB at: %s", adbPath)
|
||||||
|
|
||||||
|
client := adb.New(adbPath)
|
||||||
|
if err := client.StartServer(ctx); err != nil {
|
||||||
|
runtime.LogWarningf(ctx, "ADB start-server: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.deviceManager = device.NewManager(client)
|
||||||
|
go a.pollDevices()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Greet returns a greeting for the given name
|
func (a *App) pollDevices() {
|
||||||
func (a *App) Greet(name string) string {
|
ticker := time.NewTicker(1 * time.Second)
|
||||||
return fmt.Sprintf("Hello %s, It's show time!", name)
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-a.ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
devices, err := a.deviceManager.List(a.ctx)
|
||||||
|
if err != nil {
|
||||||
|
runtime.LogWarningf(a.ctx, "device poll: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
runtime.EventsEmit(a.ctx, "devices:changed", devices)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListDevices returns the current list of connected devices.
|
||||||
|
func (a *App) ListDevices() ([]device.Device, error) {
|
||||||
|
if a.deviceManager == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return a.deviceManager.List(a.ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectDevice connects to a wireless ADB device by address (e.g. "192.168.1.5:5555").
|
||||||
|
func (a *App) ConnectDevice(address string) error {
|
||||||
|
if a.deviceManager == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return a.deviceManager.Connect(a.ctx, address)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisconnectDevice disconnects a device by its serial ID.
|
||||||
|
func (a *App) DisconnectDevice(deviceID string) error {
|
||||||
|
if a.deviceManager == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return a.deviceManager.Disconnect(a.ctx, deviceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDeviceInfo fetches static device information for the given device ID.
|
||||||
|
func (a *App) GetDeviceInfo(deviceID string) (*device.DeviceInfo, error) {
|
||||||
|
if a.deviceManager == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return a.deviceManager.Info(a.ctx, deviceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDeviceLiveStats fetches dynamic device stats (RAM, battery, thermal, storage, IP).
|
||||||
|
func (a *App) GetDeviceLiveStats(deviceID string) (*device.DeviceLiveStats, error) {
|
||||||
|
if a.deviceManager == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return a.deviceManager.LiveStats(a.ctx, deviceID)
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-4
@@ -1,8 +1,13 @@
|
|||||||
module git.achmad.dev/admin/droidscope/desktop
|
module git.achmad.dev/admin/droidscope/desktop
|
||||||
|
|
||||||
go 1.23.0
|
go 1.26.2
|
||||||
|
|
||||||
require github.com/wailsapp/wails/v2 v2.12.0
|
require (
|
||||||
|
git.achmad.dev/admin/droidscope/backend v0.0.0
|
||||||
|
github.com/wailsapp/wails/v2 v2.12.0
|
||||||
|
)
|
||||||
|
|
||||||
|
replace git.achmad.dev/admin/droidscope/backend => ../backend-go
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect
|
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect
|
||||||
@@ -34,5 +39,3 @@ require (
|
|||||||
golang.org/x/sys v0.30.0 // indirect
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
golang.org/x/text v0.22.0 // indirect
|
golang.org/x/text v0.22.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
// replace github.com/wailsapp/wails/v2 v2.12.0 => /Users/achmad/go/pkg/mod
|
|
||||||
|
|||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,71 @@
|
|||||||
|
package adbembed
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed all:bin
|
||||||
|
var binFS embed.FS
|
||||||
|
|
||||||
|
// Resolve returns the path to a usable adb binary.
|
||||||
|
// Order: embedded binary → ANDROID_HOME → PATH.
|
||||||
|
func Resolve() (string, error) {
|
||||||
|
if path, err := extractEmbedded(); err == nil {
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
return findOnSystem()
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractEmbedded() (string, error) {
|
||||||
|
name := embeddedName()
|
||||||
|
data, err := binFS.ReadFile("bin/" + name)
|
||||||
|
if err != nil || len(data) < 1024 {
|
||||||
|
return "", fmt.Errorf("no embedded binary")
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheDir, err := os.UserCacheDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
dir := filepath.Join(cacheDir, "droidscope", "adb")
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
dest := filepath.Join(dir, adbExeName())
|
||||||
|
if err := os.WriteFile(dest, data, 0755); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return dest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findOnSystem() (string, error) {
|
||||||
|
if home := os.Getenv("ANDROID_HOME"); home != "" {
|
||||||
|
candidate := filepath.Join(home, "platform-tools", adbExeName())
|
||||||
|
if _, err := os.Stat(candidate); err == nil {
|
||||||
|
return candidate, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fall back to PATH
|
||||||
|
return findInPath()
|
||||||
|
}
|
||||||
|
|
||||||
|
func embeddedName() string {
|
||||||
|
os_ := runtime.GOOS
|
||||||
|
arch := runtime.GOARCH
|
||||||
|
if os_ == "windows" {
|
||||||
|
return fmt.Sprintf("adb-%s-%s.exe", os_, arch)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("adb-%s-%s", os_, arch)
|
||||||
|
}
|
||||||
|
|
||||||
|
func adbExeName() string {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
return "adb.exe"
|
||||||
|
}
|
||||||
|
return "adb"
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package adbembed
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
func findInPath() (string, error) {
|
||||||
|
return exec.LookPath("adb")
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package adbembed
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
func findInPath() (string, error) {
|
||||||
|
return exec.LookPath("adb.exe")
|
||||||
|
}
|
||||||
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
"frontend:dev:watcher": "npm run dev",
|
"frontend:dev:watcher": "npm run dev",
|
||||||
"frontend:dev:serverUrl": "auto",
|
"frontend:dev:serverUrl": "auto",
|
||||||
"frontend:dir": "../frontend-react",
|
"frontend:dir": "../frontend-react",
|
||||||
"wailsjsdir": "../frontend-react/src/wailsjs",
|
"wailsjsdir": "../frontend-react/src",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "achmad",
|
"name": "achmad",
|
||||||
"email": "anakinskywalk1@gmail.com"
|
"email": "anakinskywalk1@gmail.com"
|
||||||
|
|||||||
Reference in New Issue
Block a user