这是一个运行于 Android 系统(通过 ADB 连接)的控制服务,使用 Go 语言编写,支持获取设备信息、截图、UI 层级等功能。支持 REST 接口访问,可用于远程控制、自动化测试前置信息收集等场景。


✨ 功能特性

  • ✅ 获取设备品牌、型号、系统版本、序列号
  • 🖼️ 支持 minicap/screencap 截图,优先 minicap,自动 fallback
  • 🧩 获取当前 UI 层级(基于 uiautomator)
  • ❤️ 健康检查(可拓展)
  • 🧰 可远程部署和执行,日志写入本地文件

📂 目录结构要求

  • 所有运行文件位于:/data/local/tmp/myagent/
  • 需要放置以下文件:
  • myagent(本文提供的 Go 编译后可执行文件)
  • minicapminicap.so(可选,用于更快截图)

🧱 编译方式

使用 Go 交叉编译生成适用于 Android 的二进制文件:

cd D:\Nextcloud\go\myagent
$env:GOOS = "linux"
# 获取手机平台
adb -s 192.168.31.182 shell getprop ro.product.cpu.abi
$env:GOARCH = "arm64"
# 如果设备是 ARM64(大多数 Android 设备)
GOOS=linux GOARCH=arm64 go build -o myagent main.go

# 如果设备是 32 位 ARM:
# GOOS=linux GOARCH=arm go build -o myagent main.go

go build -o myagent

🚀 推送与启动

将可执行文件推送到设备:

adb -s 192.168.31.182:5555 push myagent /data/local/tmp/myagent/
adb -s 192.168.31.182 shell chmod 755 /data/local/tmp/myagent/myagent
# 后台运行
adb -s 192.168.31.182 shell "nohup /data/local/tmp/myagent/myagent > /dev/null 2>&1 &" 
# 杀死进程
adb -s 192.168.31.182:5555 shell pkill myagent 

❌ 停止服务

使用 pkill 结束运行:

adb -s 192.168.31.182:5555 shell pkill myagent

🧪 接口测试示例

# 设备信息
curl http://<设备IP>:7979/info

# 屏幕截图(保存为PNG)
curl http://<设备IP>:7979/screenshot -o screen.png

# 获取 UI 层级(JSON 封装的 XML)
curl http://<设备IP>:7979/dump/hierarchy

💻 完整源代码(main.go

package main

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "os/exec"
    "runtime"
    "strings"
)

type DeviceInfo struct {
    Brand     string `json:"brand"`
    Model     string `json:"model"`
    Serial    string `json:"serial"`
    OSVersion string `json:"os_version"`
}

const (
    ListenPort    = 7979
    WorkDirectory = "/data/local/tmp/myagent"
)

type MinicapInfo struct {
    Width    int    `json:"width"`
    Height   int    `json:"height"`
    Rotation int    `json:"rotation"`
}

func isServiceRunning(serviceName string) bool {
    cmd := exec.Command("pgrep", serviceName)
    err := cmd.Run()
    return err == nil
}

func main() {
    http.HandleFunc("/info", infoHandler)
    http.HandleFunc("/screenshot", screenshotHandler)
    http.HandleFunc("/dump/hierarchy", dumpHandler)

    addr := fmt.Sprintf(":%d", ListenPort)
    log.Printf("服务启动: %s, 工作目录: %s", addr, WorkDirectory)
    log.Fatal(http.ListenAndServe(addr, nil))
}

func infoHandler(w http.ResponseWriter, r *http.Request) {
    props := map[string]*string{
        "ro.product.brand":         new(string),
        "ro.product.model":         new(string),
        "ro.serialno":              new(string),
        "ro.build.version.release": new(string),
    }
    for prop, ptr := range props {
        out, err := exec.Command("getprop", prop).Output()
        if err == nil {
            *ptr = strings.TrimSpace(string(out))
        }
    }
    info := DeviceInfo{
        Brand:     *props["ro.product.brand"],
        Model:     *props["ro.product.model"],
        Serial:    *props["ro.serialno"],
        OSVersion: *props["ro.build.version.release"],
    }
    w.Header().Set("Content-Type", "application/json")
    if err := json.NewEncoder(w).Encode(info); err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
    }
}

func screenshotHandler(w http.ResponseWriter, r *http.Request) {
    img, err := screenshotWithMinicap()
    if err != nil {
        log.Printf("minicap 失败: %v, fallback screencap", err)
        img, err = screenshotWithScreencap()
        if err != nil {
            http.Error(w, fmt.Sprintf("截图失败: %v", err), http.StatusInternalServerError)
            return
        }
    }
    w.Header().Set("Content-Type", "image/png")
    w.Write(img)
}

func dumpHandler(w http.ResponseWriter, r *http.Request) {
    xmlContent, err := dumpHierarchy()
    if err != nil {
        log.Println("Err:", err)
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    renderJSON(w, map[string]interface{}{
        "jsonrpc": "2.0",
        "id":      1,
        "result":  xmlContent,
    })
}

func dumpHierarchy() (xmlContent string, err error) {
    const targetPath = "/sdcard/window_dump.xml"
    data, err := ioutil.ReadFile(targetPath)
    if err != nil {
        return "", fmt.Errorf("读取文件失败: %v", err)
    }
    xmlContent = string(data)
    return
}

func renderJSON(w http.ResponseWriter, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    if err := json.NewEncoder(w).Encode(data); err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
    }
}

func screenshotWithMinicap() ([]byte, error) {
    infoJSON, err := exec.Command("sh", "-c", fmt.Sprintf("LD_LIBRARY_PATH=%s /data/local/tmp/myagent/minicap -i", WorkDirectory)).Output()
    if err != nil {
        return nil, fmt.Errorf("minicap info 失败: %v", err)
    }
    var info MinicapInfo
    if err := json.Unmarshal(infoJSON, &info); err != nil {
        return nil, fmt.Errorf("解析 minicap info 失败: %v", err)
    }
    size := fmt.Sprintf("%dx%d", info.Width, info.Height)
    param := fmt.Sprintf("%s@%s/%d", size, size, info.Rotation)
    cmdStr := fmt.Sprintf("LD_LIBRARY_PATH=%s /data/local/tmp/myagent/minicap -P %s -s", WorkDirectory, param)
    img, err := exec.Command("sh", "-c", cmdStr).Output()
    if err != nil {
        return nil, fmt.Errorf("minicap 执行失败: %v", err)
    }
    return img, nil
}

func screenshotWithScreencap() ([]byte, error) {
    img, err := exec.Command("screencap", "-p").Output()
    if err != nil {
        return nil, fmt.Errorf("screencap 失败: %v", err)
    }
    return img, nil
}

func init() {
    if runtime.GOOS != "linux" {
        log.Printf("警告: 当前 OS=%s, 建议在 Android/Linux 上运行", runtime.GOOS)
    }
}