Android 控制服务(Go 实现)
这是一个运行于 Android 系统(通过 ADB 连接)的控制服务,使用 Go 语言编写,支持获取设备信息、截图、UI 层级等功能。支持 REST 接口访问,可用于远程控制、自动化测试前置信息收集等场景。
✨ 功能特性
- ✅ 获取设备品牌、型号、系统版本、序列号
- 🖼️ 支持 minicap/screencap 截图,优先 minicap,自动 fallback
- 🧩 获取当前 UI 层级(基于 uiautomator)
- ❤️ 健康检查(可拓展)
- 🧰 可远程部署和执行,日志写入本地文件
📂 目录结构要求
- 所有运行文件位于:
/data/local/tmp/myagent/
- 需要放置以下文件:
myagent
(本文提供的 Go 编译后可执行文件)minicap
、minicap.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)
}
}
本文作者: 永生
本文链接: https://yys.zone/detail/?id=418
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!
评论列表 (0 条评论)