This commit is contained in:
eric
2026-03-28 13:27:53 -05:00
parent d34ecab19c
commit 8053c4cb6d
48 changed files with 3527 additions and 1433 deletions

View File

@@ -4,7 +4,9 @@ import asyncio
import json
import re
import shutil
import tempfile
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import Optional
from urllib.parse import quote
@@ -293,13 +295,42 @@ async def android_ui_nodes(serial: str, limit: int = 80) -> list[dict]:
return nodes[: max(1, min(limit, 200))]
def _is_png(data: bytes) -> bool:
return bool(data and len(data) > 8 and data.startswith(b"\x89PNG\r\n\x1a\n"))
async def android_screenshot(serial: str) -> bytes:
if not serial:
raise ValueError("Missing Android device serial")
code, data, err = await _adb_binary(serial, "exec-out", "screencap", "-p", timeout=20.0)
if code != 0 or not data:
raise RuntimeError(err or "Failed to capture screenshot")
return data.replace(b"\r\n", b"\n")
if code == 0 and data:
normalized = data.replace(b"\r\n", b"\n")
if _is_png(data) or normalized.startswith(b"\x89PNG"):
return normalized
last_err = err or "exec-out screencap failed"
for remote in ("/data/local/tmp/live-cap.png", "/sdcard/live-cap.png"):
tmp = Path(tempfile.mkstemp(suffix=".png")[1])
try:
sh_code, _, sh_err = await _adb_text(serial, "shell", f"screencap -p {remote}", timeout=25.0)
if sh_code != 0:
last_err = sh_err or f"screencap to {remote} failed"
continue
pull_code, _, pull_err = await _run_cmd(
_adb_prefix(serial) + ["pull", remote, str(tmp)],
timeout=35.0,
)
if pull_code != 0:
last_err = pull_err or "adb pull failed"
continue
pulled = tmp.read_bytes()
if _is_png(pulled) or pulled.startswith(b"\x89PNG"):
return pulled.replace(b"\r\n", b"\n")
last_err = "Pulled file is not a valid PNG"
finally:
tmp.unlink(missing_ok=True)
raise RuntimeError(last_err)
def _normalize_text(text: str) -> str:
@@ -420,4 +451,12 @@ async def android_action(action: str, serial: str, payload: Optional[dict] = Non
output = await _shell(serial, f"pm clear '{package}'", timeout=25.0)
return {"message": f"Cleared app data: {package}", "output": output}
if action == "install_apk":
apk_path = str(payload.get("path", "")).strip()
if not apk_path or any(ch in apk_path for ch in ";$`&|()<>\n\r"):
raise ValueError("Invalid apk path on device (use adb push to /sdcard/ first)")
apk_path = apk_path.replace("'", "'\\''")
output = await _shell(serial, f"pm install -r -t '{apk_path}'", timeout=180.0)
return {"message": "APK install finished", "output": output}
raise ValueError(f"Unsupported Android action: {action}")