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:
+80
-9
@@ -2,26 +2,97 @@ package main
|
||||
|
||||
import (
|
||||
"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 {
|
||||
ctx context.Context
|
||||
ctx context.Context
|
||||
deviceManager *device.Manager
|
||||
}
|
||||
|
||||
// NewApp creates a new App application struct
|
||||
func NewApp() *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) {
|
||||
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) Greet(name string) string {
|
||||
return fmt.Sprintf("Hello %s, It's show time!", name)
|
||||
func (a *App) pollDevices() {
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
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
|
||||
|
||||
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 (
|
||||
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/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:serverUrl": "auto",
|
||||
"frontend:dir": "../frontend-react",
|
||||
"wailsjsdir": "../frontend-react/src/wailsjs",
|
||||
"wailsjsdir": "../frontend-react/src",
|
||||
"author": {
|
||||
"name": "achmad",
|
||||
"email": "anakinskywalk1@gmail.com"
|
||||
|
||||
Reference in New Issue
Block a user