diff --git a/aria-brain/agent.py b/aria-brain/agent.py
index 5561dda..e2a57e6 100644
--- a/aria-brain/agent.py
+++ b/aria-brain/agent.py
@@ -1270,7 +1270,8 @@ META_TOOLS = [
"description": (
"Zeigt die ARIA-Host-Agenten (Rechner, auf denen ein Agent DIREKT "
"laeuft und sich per RVS meldet) die ONLINE sind + was sie koennen "
- "(exec/read/write/info/screenshot). Ein Host-Agent gibt Dir vollen "
+ "(exec/read/write/info/screenshot/ui_dump; Android-Agenten koennen "
+ "screenshot+ui_dump). Ein Host-Agent gibt Dir vollen "
"Zugriff auf GENAU DIESEN Rechner — auch wenn er hinter NAT/Firewall "
"sitzt. Nutze das, wenn Stefan etwas 'auf meinem Laptop/PC/Server X' "
"machen will, das kein Geraet im Netz ist."
@@ -1364,6 +1365,25 @@ META_TOOLS = [
},
},
},
+ {
+ "type": "function",
+ "function": {
+ "name": "host_ui_dump",
+ "description": (
+ "Liest die sichtbaren Bedienelemente eines Host-Agenten als strukturierte "
+ "Liste (Text, Beschriftung, Klasse, Bildschirm-Position x/y, Rahmen, ob "
+ "klickbar/editierbar). Vor allem fuer Android-Agenten: ergaenzt "
+ "host_screenshot — der Screenshot zeigt Dir das Bild, ui_dump liefert die "
+ "exakten Element-Texte und Koordinaten, um spaeter gezielt zu tippen. Setzt "
+ "auf dem Geraet eine aktive Bedienungshilfe voraus."
+ ),
+ "parameters": {
+ "type": "object",
+ "properties": {"host": {"type": "string", "description": "Host-ID/Name."}},
+ "required": ["host"],
+ },
+ },
+ },
{
"type": "function",
"function": {
@@ -2652,6 +2672,20 @@ class Agent:
f"[FILE: {fp}] in deine Antwort — dann erscheint das Bild inline im Chat."
)
+ if name == "host_ui_dump":
+ result = _post("/internal/host", {"host": host, "action": "ui_dump",
+ "params": {}}, 20)
+ if not result.get("ok"):
+ return f"FEHLER: {result.get('error')}"
+ r = result.get("result") or {}
+ nodes = r.get("nodes") or []
+ return (
+ f"UI-Baum von {host} (App: {r.get('package', '?')}, "
+ f"{r.get('count', len(nodes))} Elemente). Jeder Eintrag hat x/y = "
+ f"Mittelpunkt zum Antippen:\n"
+ + json.dumps(nodes, ensure_ascii=False, indent=2)
+ )
+
return f"FEHLER: unbekanntes Host-Tool {name}"
except Exception as exc:
return f"FEHLER: Host/Bridge nicht erreichbar: {exc}"
@@ -3449,7 +3483,7 @@ class Agent:
if name in ("satellite_list", "satellite_devices", "satellite_command"):
return self._dispatch_satellite(name, arguments)
if name in ("host_list", "host_exec", "host_read", "host_write",
- "host_info", "host_screenshot"):
+ "host_info", "host_screenshot", "host_ui_dump"):
return self._dispatch_host(name, arguments)
if name == "vm_register":
pid = (project_id or "").strip()
diff --git a/host-agent/android/README.md b/host-agent/android/README.md
index b8b0a9b..89bd2f4 100644
--- a/host-agent/android/README.md
+++ b/host-agent/android/README.md
@@ -103,11 +103,14 @@ identisch, nur der Wecker ändert sich.
## Meilensteine
-1. **Verbinden + sichtbar** — Gradle-Projekt, AndroidManifest, RVS-WS-Client,
+1. **✅ Verbinden + sichtbar** — Gradle-Projekt, AndroidManifest, RVS-WS-Client,
Foreground-Service, Connect-UI (QR-Scan + manuell), `host_hello`/`host_ping`.
- → Agent erscheint in der Diagnostic. Noch keine Steuerung.
-2. **Sehen** — MediaProjection-Screenshot + `ui_dump` (AccessibilityService,
- read-only). → ARIA kann den Schirm ansehen und beschreiben.
+ → Agent erscheint in der Diagnostic. `info` funktioniert.
+2. **✅ Sehen** — MediaProjection-Screenshot (`ScreenCapturer`, gleicher
+ `{format,bytes,base64}`-Vertrag wie der Desktop-Agent → `host_screenshot`) +
+ `ui_dump` (`AriaAccessibilityService`, nur lesend → Brain-Tool `host_ui_dump`).
+ Freigabe einmalig in der App: „Bildschirm-Zugriff erlauben" + „Bedienungshilfe
+ öffnen". → ARIA sieht den Schirm und liest die UI-Elemente mit Koordinaten.
3. **Steuern** — `ui_tap`/`ui_text`/`ui_swipe`/`ui_key`/`app_launch` über den
AccessibilityService. → ARIA bedient Apps (E-Mail-Setup).
4. **Feinschliff** — `info`/`app_list`/`notify`, Build-Härtung, `release.sh`
@@ -124,4 +127,5 @@ cd host-agent/android
APK wird manuell aufs Zielgerät kopiert + installiert (unbekannte Quellen
erlauben). Gitea-Release macht den Download einfach (wie die Haupt-App).
-> Status: **Design.** Als Nächstes Meilenstein 1 (Verbinden + sichtbar).
+> Status: **M1 + M2 fertig** (verbinden, `info`, `screenshot`, `ui_dump`).
+> Als Nächstes Meilenstein 3 (Steuern).
diff --git a/host-agent/android/app/src/main/AndroidManifest.xml b/host-agent/android/app/src/main/AndroidManifest.xml
index c165528..ba6755b 100644
--- a/host-agent/android/app/src/main/AndroidManifest.xml
+++ b/host-agent/android/app/src/main/AndroidManifest.xml
@@ -39,5 +39,18 @@
+
+
+
+
+
+
+
diff --git a/host-agent/android/app/src/main/java/de/hackersoft/ariaagent/AgentService.kt b/host-agent/android/app/src/main/java/de/hackersoft/ariaagent/AgentService.kt
index 4908726..96fcdbd 100644
--- a/host-agent/android/app/src/main/java/de/hackersoft/ariaagent/AgentService.kt
+++ b/host-agent/android/app/src/main/java/de/hackersoft/ariaagent/AgentService.kt
@@ -23,6 +23,9 @@ class AgentService : Service() {
private const val CH = "aria_agent"
private const val NOTIF_ID = 1
const val ACTION_STATUS = "de.hackersoft.ariaagent.STATUS"
+ const val ACTION_PROJECTION = "de.hackersoft.ariaagent.PROJECTION"
+ const val EXTRA_RESULT_CODE = "resultCode"
+ const val EXTRA_RESULT_DATA = "resultData"
@Volatile var status: String = "gestoppt"
@Volatile var connected: Boolean = false
@@ -44,6 +47,19 @@ class AgentService : Service() {
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ if (intent?.action == ACTION_PROJECTION) {
+ val code = intent.getIntExtra(EXTRA_RESULT_CODE, 0)
+ @Suppress("DEPRECATION")
+ val data = intent.getParcelableExtra(EXTRA_RESULT_DATA)
+ if (code != 0 && data != null) ScreenCapturer.start(applicationContext, code, data)
+ if (rvs == null) startRvs() // Dienst war frisch -> Verbindung nachziehen
+ return START_STICKY
+ }
+ return startRvs()
+ }
+
+ /** (Re-)Startet die RVS-Verbindung anhand der gespeicherten Config. */
+ private fun startRvs(): Int {
val cfg = AgentConfig.load(this)
if (!cfg.isValid()) {
stopSelf()
@@ -62,6 +78,7 @@ class AgentService : Service() {
override fun onDestroy() {
rvs?.stop()
+ ScreenCapturer.stop()
connected = false
status = "gestoppt"
sendBroadcast(Intent(ACTION_STATUS).setPackage(packageName))
diff --git a/host-agent/android/app/src/main/java/de/hackersoft/ariaagent/AriaAccessibilityService.kt b/host-agent/android/app/src/main/java/de/hackersoft/ariaagent/AriaAccessibilityService.kt
new file mode 100644
index 0000000..b76aed5
--- /dev/null
+++ b/host-agent/android/app/src/main/java/de/hackersoft/ariaagent/AriaAccessibilityService.kt
@@ -0,0 +1,91 @@
+package de.hackersoft.ariaagent
+
+import android.accessibilityservice.AccessibilityService
+import android.graphics.Rect
+import android.view.accessibility.AccessibilityEvent
+import android.view.accessibility.AccessibilityNodeInfo
+import org.json.JSONArray
+import org.json.JSONObject
+
+/**
+ * Bedienungshilfe-Dienst (Meilenstein 2 — nur LESEND).
+ *
+ * Liefert einen strukturierten Baum der sichtbaren Bildschirm-Elemente: Text,
+ * Beschriftung (contentDescription), Klasse, Bildschirm-Position (Mittelpunkt +
+ * Rahmen) und Flags (klickbar/editierbar/ankreuzbar). ARIA nutzt das ergaenzend
+ * zum Screenshot, um Ziele exakt zu benennen. Tippen/Text folgt in M3.
+ *
+ * Der Nutzer schaltet den Dienst einmalig unter Einstellungen > Bedienungshilfen
+ * frei. Er fuehrt hier nichts autonom aus — reagiert nur auf `dump()`.
+ */
+class AriaAccessibilityService : AccessibilityService() {
+
+ override fun onServiceConnected() {
+ instance = this
+ }
+
+ override fun onUnbind(intent: android.content.Intent?): Boolean {
+ if (instance === this) instance = null
+ return super.onUnbind(intent)
+ }
+
+ override fun onDestroy() {
+ if (instance === this) instance = null
+ super.onDestroy()
+ }
+
+ override fun onAccessibilityEvent(event: AccessibilityEvent?) { /* passiv */ }
+ override fun onInterrupt() { /* passiv */ }
+
+ /** Strukturierter Baum des aktiven Fensters. Shape: {ok, result:{package,count,nodes[]}}. */
+ fun dump(): JSONObject {
+ val root = rootInActiveWindow
+ ?: return JSONObject().put("ok", false)
+ .put("error", "Kein aktives Fenster lesbar (Bildschirm evtl. aus oder gesperrt).")
+ val nodes = JSONArray()
+ try {
+ walk(root, nodes, 0)
+ } finally {
+ @Suppress("DEPRECATION") try { root.recycle() } catch (_: Exception) {}
+ }
+ val result = JSONObject()
+ .put("package", root.packageName?.toString() ?: "")
+ .put("count", nodes.length())
+ .put("nodes", nodes)
+ return JSONObject().put("ok", true).put("result", result)
+ }
+
+ private fun walk(node: AccessibilityNodeInfo?, out: JSONArray, depth: Int) {
+ if (node == null || depth > 40 || out.length() >= 400) return
+ val text = node.text?.toString()?.trim()
+ val desc = node.contentDescription?.toString()?.trim()
+ val cls = node.className?.toString()?.substringAfterLast('.')
+ val interesting = !text.isNullOrBlank() || !desc.isNullOrBlank() ||
+ node.isClickable || node.isEditable || node.isCheckable
+ if (interesting) {
+ val r = Rect()
+ node.getBoundsInScreen(r)
+ val o = JSONObject()
+ if (!text.isNullOrBlank()) o.put("text", text)
+ if (!desc.isNullOrBlank()) o.put("desc", desc)
+ if (cls != null) o.put("cls", cls)
+ if (node.isClickable) o.put("clickable", true)
+ if (node.isEditable) o.put("editable", true)
+ if (node.isCheckable) o.put("checked", node.isChecked)
+ o.put("x", r.centerX())
+ o.put("y", r.centerY())
+ o.put("bounds", "${r.left},${r.top},${r.right},${r.bottom}")
+ out.put(o)
+ }
+ for (i in 0 until node.childCount) {
+ walk(node.getChild(i), out, depth + 1)
+ }
+ }
+
+ companion object {
+ @Volatile
+ var instance: AriaAccessibilityService? = null
+
+ val available: Boolean get() = instance != null
+ }
+}
diff --git a/host-agent/android/app/src/main/java/de/hackersoft/ariaagent/MainActivity.kt b/host-agent/android/app/src/main/java/de/hackersoft/ariaagent/MainActivity.kt
index dbd00c2..a33e54c 100644
--- a/host-agent/android/app/src/main/java/de/hackersoft/ariaagent/MainActivity.kt
+++ b/host-agent/android/app/src/main/java/de/hackersoft/ariaagent/MainActivity.kt
@@ -1,13 +1,16 @@
package de.hackersoft.ariaagent
import android.Manifest
+import android.app.Activity
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
+import android.media.projection.MediaProjectionManager
import android.os.Build
import android.os.Bundle
+import android.provider.Settings
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
@@ -38,6 +41,21 @@ class MainActivity : AppCompatActivity() {
}
private val notifPerm = registerForActivityResult(ActivityResultContracts.RequestPermission()) { }
+ private val projection = registerForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) { res ->
+ if (res.resultCode == Activity.RESULT_OK && res.data != null) {
+ val i = Intent(this, AgentService::class.java)
+ .setAction(AgentService.ACTION_PROJECTION)
+ .putExtra(AgentService.EXTRA_RESULT_CODE, res.resultCode)
+ .putExtra(AgentService.EXTRA_RESULT_DATA, res.data)
+ ContextCompat.startForegroundService(this, i)
+ toast("Bildschirm-Zugriff aktiv — ARIA kann jetzt Screenshots machen")
+ } else {
+ toast("Bildschirm-Zugriff abgelehnt")
+ }
+ }
+
private val statusReceiver = object : BroadcastReceiver() {
override fun onReceive(c: Context?, i: Intent?) = refreshStatus()
}
@@ -64,6 +82,22 @@ class MainActivity : AppCompatActivity() {
AgentService.stop(this)
refreshStatus()
}
+ findViewById