Compare commits
7 Commits
1359b1adb3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e8b31eb263 | |||
| 53cf344cc0 | |||
| 40af079fd2 | |||
| 4304a92a53 | |||
| cce42f7c2d | |||
| 050ee6f0f7 | |||
| d1da3deb8d |
63
adb.go
Normal file
63
adb.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package goadb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Pair a device using the 6 digit code
|
||||||
|
func PairDevice(endpoint string, code string) error {
|
||||||
|
_, err := ExecuteCommand(fmt.Sprintf("pair %s %s", endpoint, code))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to a device
|
||||||
|
func ConnectDevice(endpoint string) (*Device,error) {
|
||||||
|
//todo: check if the device is paired before connecting
|
||||||
|
_, err := ExecuteCommand("connect " + endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return nil,err
|
||||||
|
}
|
||||||
|
|
||||||
|
d := &Device{Serial: endpoint}
|
||||||
|
return d,err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the already connected adb devices
|
||||||
|
func GetConnectedDevices() ([]Device, error) {
|
||||||
|
var devices []Device
|
||||||
|
|
||||||
|
output, err := ExecuteCommand("devices")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to determine device status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(string(output), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.Contains(line, "device") && !strings.Contains(line, "List of") {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) >= 2 && fields[1] == "device" {
|
||||||
|
devices = append(devices, Device{Serial: fields[0]})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return devices, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute a raw adb command and return the output
|
||||||
|
func ExecuteCommand(command string) ([]byte, error) {
|
||||||
|
|
||||||
|
//Check if adb is available
|
||||||
|
if _, err := exec.LookPath("adb"); err != nil {
|
||||||
|
return nil, fmt.Errorf("adb binary not found in PATH")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("sh", "-c", "adb "+command)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("command failed: %v", err)
|
||||||
|
}
|
||||||
|
return output, nil
|
||||||
|
}
|
||||||
61
app.go
Normal file
61
app.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package goadb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get the current focussed app package name
|
||||||
|
func (d *Device) GetCurrentAppPackage() (string, error) {
|
||||||
|
data, err := d.RunCommand("shell dumpsys activity activities | grep ResumedActivity")
|
||||||
|
re := regexp.MustCompile(`\b([a-zA-Z0-9._]+)/(?:[a-zA-Z0-9._]+)\b`)
|
||||||
|
matches := re.FindStringSubmatch(string(data))
|
||||||
|
|
||||||
|
if len(matches) > 1 {
|
||||||
|
return matches[1], nil
|
||||||
|
} else {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the pid for a app
|
||||||
|
func (d *Device) GetAppPid(appackage string) (int, error) {
|
||||||
|
data, err := d.RunCommand(fmt.Sprintf("shell pidof %s"))
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pid, err := strconv.Atoi(strings.TrimSpace(string(data)))
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return pid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a app is running
|
||||||
|
func (d *Device) CheckAppRunning(appackage string) (bool, error) {
|
||||||
|
pid, err := d.GetAppPid(appackage)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if pid != -1 {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start a app using the package name
|
||||||
|
func (d *Device) StartApp(appackage string) error {
|
||||||
|
_, err := d.RunCommand(fmt.Sprintf("shell monkey -p %s -c android.intent.category.LAUNCHER 1", appackage))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force kill a running app
|
||||||
|
func (d *Device) KillApp(appackage string) error {
|
||||||
|
_, err := d.RunCommand(fmt.Sprintf("shell am force-stop %s", appackage))
|
||||||
|
return err
|
||||||
|
}
|
||||||
30
device.go
Normal file
30
device.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package goadb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsConnected checks if the device is currently connected via ADB
|
||||||
|
func (d *Device) IsConnected() bool {
|
||||||
|
output, err := ExecuteCommand("devices")
|
||||||
|
if err != nil {
|
||||||
|
log.Println(fmt.Errorf("Failed to determine device status: %w", err))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
lines := strings.Split(string(output), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.HasPrefix(line, d.Serial) && strings.Contains(line, "device") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run a command on this specific device
|
||||||
|
func (d *Device) RunCommand(command string) ([]byte, error) {
|
||||||
|
return ExecuteCommand(fmt.Sprintf("-s %s %s", d.Serial, command))
|
||||||
|
}
|
||||||
|
|
||||||
5
go.mod
Normal file
5
go.mod
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module git.plabble.org/misthios/goadb
|
||||||
|
|
||||||
|
go 1.24.6
|
||||||
|
|
||||||
|
require github.com/otiai10/gosseract/v2 v2.4.1
|
||||||
155
screen.go
Normal file
155
screen.go
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
package goadb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"image/png"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/otiai10/gosseract/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get the state of the screen (Asleep/Awake)
|
||||||
|
func (d *Device) GetScreenState() (string, error) {
|
||||||
|
data, err := d.RunCommand("shell dumpsys power | grep mWakefulness | head -n1")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
state := strings.Split(string(data), "=")[1]
|
||||||
|
return state, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle the screen by simulating a press of the power button
|
||||||
|
func (d *Device) ToggleScreen() error {
|
||||||
|
_, err := d.RunCommand("shell input keyevent 26")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tap on a location on the screen
|
||||||
|
func (d *Device) TapScreen(x int, y int) error {
|
||||||
|
_, err := d.RunCommand(fmt.Sprintf("shell input tap %d %d", x, y))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the resolution of the screen
|
||||||
|
func (d *Device) GetScreenResolution() (int, int, error) {
|
||||||
|
|
||||||
|
//Not yet set on the device so use adb to retrieve it
|
||||||
|
if d.Screenx == 0 || d.Screeny == 0 {
|
||||||
|
|
||||||
|
data, err := d.RunCommand("shell wm size")
|
||||||
|
res := strings.Split(string(data), ":")[1]
|
||||||
|
dimensions := strings.Split(res, "x")
|
||||||
|
|
||||||
|
if len(dimensions) != 2 {
|
||||||
|
return -1, -1, fmt.Errorf("invalid dimensions for screen")
|
||||||
|
}
|
||||||
|
|
||||||
|
x, err := strconv.Atoi(strings.TrimSpace(dimensions[0]))
|
||||||
|
if err != nil {
|
||||||
|
return -1, -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
y, err := strconv.Atoi(strings.TrimSpace(dimensions[1]))
|
||||||
|
if err != nil {
|
||||||
|
return -1, -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Screenx = x
|
||||||
|
d.Screeny = y
|
||||||
|
|
||||||
|
return x, y, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return d.Screenx, d.Screeny, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Device) GetScreenText(xStart int, xEnd int, yStart int, yEnd int, whitelist string, multiline bool, textPolarity string) (string, error) {
|
||||||
|
data, err := d.RunCommand("exec-out screencap -p")
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to take screenshot: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
img, err := png.Decode(bytes.NewReader(data))
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to decode PNG: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rect := image.Rect(0, 0, xEnd-xStart, yEnd-yStart)
|
||||||
|
cropped := image.NewRGBA(rect)
|
||||||
|
for y := yStart; y < yEnd; y++ {
|
||||||
|
for x := xStart; x < xEnd; x++ {
|
||||||
|
cropped.Set(x-xStart, y-yStart, img.At(x, y))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to grayscale and compute average brightness
|
||||||
|
gray := image.NewGray(rect)
|
||||||
|
var totalBrightness int
|
||||||
|
for y := 0; y < rect.Dy(); y++ {
|
||||||
|
for x := 0; x < rect.Dx(); x++ {
|
||||||
|
grayVal := color.GrayModel.Convert(cropped.At(x, y)).(color.Gray)
|
||||||
|
gray.Set(x, y, grayVal)
|
||||||
|
totalBrightness += int(grayVal.Y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
avgBrightness := totalBrightness / (rect.Dx() * rect.Dy())
|
||||||
|
|
||||||
|
// Decide polarity
|
||||||
|
useLightText := false
|
||||||
|
switch textPolarity {
|
||||||
|
case "light":
|
||||||
|
useLightText = true
|
||||||
|
case "dark":
|
||||||
|
useLightText = false
|
||||||
|
case "auto":
|
||||||
|
useLightText = avgBrightness < 128
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("invalid textPolarity: must be 'dark', 'light', or 'auto'")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Binarize based on polarity
|
||||||
|
binarized := image.NewGray(rect)
|
||||||
|
for y := 0; y < rect.Dy(); y++ {
|
||||||
|
for x := 0; x < rect.Dx(); x++ {
|
||||||
|
grayVal := gray.GrayAt(x, y).Y
|
||||||
|
if useLightText {
|
||||||
|
if int(grayVal) > avgBrightness+10 {
|
||||||
|
binarized.SetGray(x, y, color.Gray{Y: 0})
|
||||||
|
} else {
|
||||||
|
binarized.SetGray(x, y, color.Gray{Y: 255})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if int(grayVal) < avgBrightness-10 {
|
||||||
|
binarized.SetGray(x, y, color.Gray{Y: 0})
|
||||||
|
} else {
|
||||||
|
binarized.SetGray(x, y, color.Gray{Y: 255})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := png.Encode(&buf, binarized); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to encode image: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := gosseract.NewClient()
|
||||||
|
defer client.Close()
|
||||||
|
client.SetImageFromBytes(buf.Bytes())
|
||||||
|
client.SetWhitelist(whitelist)
|
||||||
|
if multiline {
|
||||||
|
client.SetPageSegMode(gosseract.PSM_AUTO)
|
||||||
|
} else {
|
||||||
|
client.SetPageSegMode(gosseract.PSM_SINGLE_BLOCK)
|
||||||
|
}
|
||||||
|
|
||||||
|
text, err := client.Text()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("tesseract error: %w", err)
|
||||||
|
}
|
||||||
|
return text, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user