Compare commits

...
4 Commits
Author SHA1 Message Date
duffyduck 5941be0f21 release(agent): bump to 0.0.0.6 2026-09-24 21:57:38 +02:00
duffyduckandClaude Opus 4.8 2038b676e6 feat(android-agent): Meilenstein 3 — steuern (tap/text/swipe/key/app_launch)
Bedienungshilfe kann jetzt bedienen: canPerformGestures + dispatchGesture (tap/
swipe), ACTION_SET_TEXT (text), performGlobalAction (back/home/recents/notif).
app_launch via getLaunchIntentForPackage (+ Label-Suche, QUERY_ALL_PACKAGES).
caps erweitert. Brain-Tools host_ui_tap/_text/_swipe/_key/host_app_launch ->
generischer _UI_ACTIONS-Dispatch. Tap-Koordinaten = ui_dump-x/y (echte Pixel,
nicht der skalierte Screenshot). Damit voller Ablauf sehen->steuern. Version 0.0.0.6.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-24 21:53:02 +02:00
duffyduck 8b84ee3b9c release(agent): bump to 0.0.0.5 2026-09-24 21:44:05 +02:00
duffyduckandClaude Opus 4.8 c86cd59b5c fix(android-agent): Screenshot — Vordergrund-Dienst-Typ mediaProjection
Geraet (Android 12) meldete beim Projizieren:
'Media projections require a foreground service ...
FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION'. Der Dienst lief als reiner dataSync.
Fix: foregroundServiceType=dataSync|mediaProjection + Permissions
FOREGROUND_SERVICE_MEDIA_PROJECTION/-DATA_SYNC; beim Projektions-Start
startForeground(..., MEDIA_PROJECTION|DATA_SYNC) VOR getMediaProjection().
Normale Starts nutzen explizit nur dataSync (Android-14-konform). Version 0.0.0.5.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-24 21:42:23 +02:00
9 changed files with 335 additions and 22 deletions
+122 -1
View File
@@ -1384,6 +1384,107 @@ META_TOOLS = [
}, },
}, },
}, },
{
"type": "function",
"function": {
"name": "host_ui_tap",
"description": (
"Tippt auf einem Host-Agenten (v.a. Android) auf eine Bildschirm-Position. "
"Nutze die x/y-Koordinaten AUS host_ui_dump (Element-Mittelpunkt, echte "
"Bildschirm-Pixel) — NICHT aus dem Screenshot (der ist skaliert). Ablauf: "
"erst host_ui_dump/host_screenshot (sehen), dann host_ui_tap (steuern)."
),
"parameters": {
"type": "object",
"properties": {
"host": {"type": "string", "description": "Host-ID/Name."},
"x": {"type": "integer", "description": "X (Pixel, aus ui_dump)."},
"y": {"type": "integer", "description": "Y (Pixel, aus ui_dump)."},
},
"required": ["host", "x", "y"],
},
},
},
{
"type": "function",
"function": {
"name": "host_ui_text",
"description": (
"Schreibt Text in ein Eingabefeld an Position x/y (aus host_ui_dump, "
"editable=true). Tippe ggf. vorher mit host_ui_tap ins Feld, damit es "
"fokussiert ist."
),
"parameters": {
"type": "object",
"properties": {
"host": {"type": "string", "description": "Host-ID/Name."},
"x": {"type": "integer", "description": "X des Feldes (aus ui_dump)."},
"y": {"type": "integer", "description": "Y des Feldes (aus ui_dump)."},
"text": {"type": "string", "description": "Einzugebender Text."},
},
"required": ["host", "x", "y", "text"],
},
},
},
{
"type": "function",
"function": {
"name": "host_ui_swipe",
"description": (
"Wischt/scrollt auf einem Host-Agenten von (x1,y1) nach (x2,y2). Zum "
"Scrollen nach unten z.B. von weiter unten nach weiter oben wischen."
),
"parameters": {
"type": "object",
"properties": {
"host": {"type": "string", "description": "Host-ID/Name."},
"x1": {"type": "integer"}, "y1": {"type": "integer"},
"x2": {"type": "integer"}, "y2": {"type": "integer"},
"duration_ms": {"type": "integer", "description": "Dauer in ms (Default 300)."},
},
"required": ["host", "x1", "y1", "x2", "y2"],
},
},
},
{
"type": "function",
"function": {
"name": "host_ui_key",
"description": (
"Drueckt eine globale Taste auf einem Host-Agenten (Android): "
"back, home, recents, notifications."
),
"parameters": {
"type": "object",
"properties": {
"host": {"type": "string", "description": "Host-ID/Name."},
"key": {"type": "string",
"description": "back | home | recents | notifications"},
},
"required": ["host", "key"],
},
},
},
{
"type": "function",
"function": {
"name": "host_app_launch",
"description": (
"Startet eine App auf einem Host-Agenten (Android) — per Paketname "
"(package, z.B. 'com.google.android.gm') ODER Namens-Suche (query, z.B. "
"'Einstellungen'). Danach mit host_screenshot/host_ui_dump weiterarbeiten."
),
"parameters": {
"type": "object",
"properties": {
"host": {"type": "string", "description": "Host-ID/Name."},
"package": {"type": "string", "description": "Paketname (optional)."},
"query": {"type": "string", "description": "App-Name/Teilstring (optional)."},
},
"required": ["host"],
},
},
},
{ {
"type": "function", "type": "function",
"function": { "function": {
@@ -2686,6 +2787,24 @@ class Agent:
+ json.dumps(nodes, ensure_ascii=False, indent=2) + json.dumps(nodes, ensure_ascii=False, indent=2)
) )
# ── Steuern (Meilenstein 3) ──────────────────────────────
_UI_ACTIONS = {
"host_ui_tap": ("ui_tap", ["x", "y"]),
"host_ui_text": ("ui_text", ["x", "y", "text"]),
"host_ui_swipe": ("ui_swipe", ["x1", "y1", "x2", "y2", "duration_ms"]),
"host_ui_key": ("ui_key", ["key"]),
"host_app_launch": ("app_launch", ["package", "query"]),
}
if name in _UI_ACTIONS:
action, keys = _UI_ACTIONS[name]
params = {k: arguments[k] for k in keys if arguments.get(k) is not None}
result = _post("/internal/host",
{"host": host, "action": action, "params": params}, 20)
if not result.get("ok"):
return f"FEHLER: {result.get('error')}"
r = result.get("result") or {}
return f"OK ({host}): {r.get('message', 'ausgefuehrt')}"
return f"FEHLER: unbekanntes Host-Tool {name}" return f"FEHLER: unbekanntes Host-Tool {name}"
except Exception as exc: except Exception as exc:
return f"FEHLER: Host/Bridge nicht erreichbar: {exc}" return f"FEHLER: Host/Bridge nicht erreichbar: {exc}"
@@ -3483,7 +3602,9 @@ class Agent:
if name in ("satellite_list", "satellite_devices", "satellite_command"): if name in ("satellite_list", "satellite_devices", "satellite_command"):
return self._dispatch_satellite(name, arguments) return self._dispatch_satellite(name, arguments)
if name in ("host_list", "host_exec", "host_read", "host_write", if name in ("host_list", "host_exec", "host_read", "host_write",
"host_info", "host_screenshot", "host_ui_dump"): "host_info", "host_screenshot", "host_ui_dump",
"host_ui_tap", "host_ui_text", "host_ui_swipe",
"host_ui_key", "host_app_launch"):
return self._dispatch_host(name, arguments) return self._dispatch_host(name, arguments)
if name == "vm_register": if name == "vm_register":
pid = (project_id or "").strip() pid = (project_id or "").strip()
+10 -6
View File
@@ -111,10 +111,12 @@ identisch, nur der Wecker ändert sich.
`ui_dump` (`AriaAccessibilityService`, nur lesend → Brain-Tool `host_ui_dump`). `ui_dump` (`AriaAccessibilityService`, nur lesend → Brain-Tool `host_ui_dump`).
Freigabe einmalig in der App: „Bildschirm-Zugriff erlauben" + „Bedienungshilfe Freigabe einmalig in der App: „Bildschirm-Zugriff erlauben" + „Bedienungshilfe
öffnen". → ARIA sieht den Schirm und liest die UI-Elemente mit Koordinaten. ö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 3. **✅ Steuern** — `ui_tap`/`ui_text`/`ui_swipe`/`ui_key`/`app_launch` über den
AccessibilityService. → ARIA bedient Apps (E-Mail-Setup). AccessibilityService (`canPerformGestures`, `dispatchGesture`, `ACTION_SET_TEXT`,
4. **Feinschliff** — `info`/`app_list`/`notify`, Build-Härtung, `release.sh` `performGlobalAction`) → Brain-Tools `host_ui_tap`/`_text`/`_swipe`/`_key`/
(Version-Param → Gitea-Release-Asset, wie die App). `host_app_launch`. Tap-Koordinaten = die x/y aus `ui_dump` (echte Pixel), NICHT
aus dem (skalierten) Screenshot. → ARIA bedient Apps (E-Mail-Setup).
4. **Feinschliff** — `app_list`/`notify`, Build-Härtung, `release_agent.sh`.
## Bauen ## Bauen
@@ -190,5 +192,7 @@ Gitea-Zugang (`GITEA_URL`, `GITEA_REPO`, `GITEA_USER`) kommt aus der Umgebung od
einer `.env`; das Kennwort wird interaktiv abgefragt. **Binaries landen unter einer `.env`; das Kennwort wird interaktiv abgefragt. **Binaries landen unter
„Releases", nie im Tree.** „Releases", nie im Tree.**
> Status: **M1 + M2 fertig** (verbinden, `info`, `screenshot`, `ui_dump`), > Status: **M1–M3 fertig** — verbinden, `info`, `screenshot`, `ui_dump` (sehen)
> `release_agent.sh` vorhanden. Als Nächstes Meilenstein 3 (Steuern). > und `ui_tap`/`ui_text`/`ui_swipe`/`ui_key`/`app_launch` (steuern);
> `release_agent.sh` vorhanden. Damit läuft der volle Ablauf sehen→steuern
> (z.B. E-Mail-Konto einrichten). Nächstes: Feinschliff (M4).
+2 -2
View File
@@ -11,8 +11,8 @@ android {
applicationId 'de.hackersoft.ariaagent' applicationId 'de.hackersoft.ariaagent'
minSdk 26 minSdk 26
targetSdk 33 // 33 vermeidet die Foreground-Service-Typ-Pflicht von 34 targetSdk 33 // 33 vermeidet die Foreground-Service-Typ-Pflicht von 34
versionCode 4 versionCode 6
versionName '0.0.0.4' versionName '0.0.0.6'
} }
// Fester Signaturschlüssel: jeder Build signiert mit DEMSELBEN Key, damit // Fester Signaturschlüssel: jeder Build signiert mit DEMSELBEN Key, damit
@@ -3,10 +3,14 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<!-- app_launch: Start-Intents/App-Labels sind ab Android 11 sonst unsichtbar. -->
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<uses-feature android:name="android.hardware.camera" android:required="false" /> <uses-feature android:name="android.hardware.camera" android:required="false" />
<application <application
@@ -30,7 +34,7 @@
<service <service
android:name=".AgentService" android:name=".AgentService"
android:exported="false" android:exported="false"
android:foregroundServiceType="dataSync" /> android:foregroundServiceType="dataSync|mediaProjection" />
<receiver <receiver
android:name=".BootReceiver" android:name=".BootReceiver"
@@ -7,6 +7,7 @@ import android.app.PendingIntent
import android.app.Service import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
@@ -43,7 +44,19 @@ class AgentService : Service() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
createChannel() createChannel()
startForeground(NOTIF_ID, buildNotification("startet …")) startForegroundDataSync("startet …")
}
/** Normaler Vordergrund-Start (Typ dataSync). mediaProjection wird NUR beim
* Projizieren gesetzt — auf Android 14 darf man diesen Typ sonst nicht ohne
* Projection-Token verwenden. */
private fun startForegroundDataSync(text: String) {
if (Build.VERSION.SDK_INT >= 29) {
startForeground(NOTIF_ID, buildNotification(text),
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
} else {
startForeground(NOTIF_ID, buildNotification(text))
}
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@@ -52,7 +65,7 @@ class AgentService : Service() {
// (ForegroundServiceDidNotStartInTimeException). Der Projection-Intent kommt // (ForegroundServiceDidNotStartInTimeException). Der Projection-Intent kommt
// per startForegroundService, obwohl der Dienst schon laeuft -> hier IMMER // per startForegroundService, obwohl der Dienst schon laeuft -> hier IMMER
// zuerst startForeground aufrufen (idempotent). // zuerst startForeground aufrufen (idempotent).
startForeground(NOTIF_ID, buildNotification(status)) startForegroundDataSync(status)
if (intent?.action == ACTION_PROJECTION) { if (intent?.action == ACTION_PROJECTION) {
val code = intent.getIntExtra(EXTRA_RESULT_CODE, 0) val code = intent.getIntExtra(EXTRA_RESULT_CODE, 0)
@@ -60,10 +73,24 @@ class AgentService : Service() {
val data = intent.getParcelableExtra<Intent>(EXTRA_RESULT_DATA) val data = intent.getParcelableExtra<Intent>(EXTRA_RESULT_DATA)
if (code != 0 && data != null) { if (code != 0 && data != null) {
try { try {
// Android verlangt beim Projizieren einen Vordergrund-Dienst vom
// Typ mediaProjection — VOR getMediaProjection() setzen, sonst
// SecurityException (FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION).
if (Build.VERSION.SDK_INT >= 29) {
startForeground(
NOTIF_ID, buildNotification("Bildschirm-Zugriff aktiv"),
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION or
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
)
}
ScreenCapturer.start(applicationContext, code, data) ScreenCapturer.start(applicationContext, code, data)
updateNotification("$status · Bildschirm-Zugriff aktiv") if (ScreenCapturer.active) {
updateNotification("$status · Bildschirm-Zugriff aktiv")
} else {
updateNotification("Bildschirm-Fehler: ${ScreenCapturer.lastError}")
}
} catch (e: Throwable) { } catch (e: Throwable) {
ScreenCapturer.lastError = e.message ?: e.javaClass.simpleName ScreenCapturer.lastError = "${e.javaClass.simpleName}: ${e.message}"
updateNotification("Bildschirm-Fehler: ${ScreenCapturer.lastError}") updateNotification("Bildschirm-Fehler: ${ScreenCapturer.lastError}")
} }
} }
@@ -1,11 +1,17 @@
package de.hackersoft.ariaagent package de.hackersoft.ariaagent
import android.accessibilityservice.AccessibilityService import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.GestureDescription
import android.graphics.Path
import android.graphics.Rect import android.graphics.Rect
import android.os.Bundle
import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityNodeInfo
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
/** /**
* Bedienungshilfe-Dienst (Meilenstein 2 — nur LESEND). * Bedienungshilfe-Dienst (Meilenstein 2 — nur LESEND).
@@ -82,6 +88,85 @@ class AriaAccessibilityService : AccessibilityService() {
} }
} }
// ─── Steuern (Meilenstein 3) ────────────────────────────────────
/** Tippt auf Bildschirm-Koordinaten (Pixel wie in ui_dump x/y). */
fun tap(x: Int, y: Int): JSONObject {
val path = Path().apply { moveTo(x.toFloat(), y.toFloat()) }
return gesture(path, 0, 60, "Tippen ($x,$y)")
}
/** Wischt von (x1,y1) nach (x2,y2) ueber dauerMs (Scrollen/Swipen). */
fun swipe(x1: Int, y1: Int, x2: Int, y2: Int, durationMs: Int): JSONObject {
val path = Path().apply {
moveTo(x1.toFloat(), y1.toFloat())
lineTo(x2.toFloat(), y2.toFloat())
}
return gesture(path, 0, durationMs.coerceIn(50, 5000).toLong(),
"Wischen ($x1,$y1 -> $x2,$y2)")
}
private fun gesture(path: Path, startMs: Long, durationMs: Long, desc: String): JSONObject {
val g = GestureDescription.Builder()
.addStroke(GestureDescription.StrokeDescription(path, startMs, durationMs))
.build()
val latch = CountDownLatch(1)
val ok = AtomicBoolean(false)
val dispatched = dispatchGesture(g, object : GestureResultCallback() {
override fun onCompleted(d: GestureDescription?) { ok.set(true); latch.countDown() }
override fun onCancelled(d: GestureDescription?) { latch.countDown() }
}, null)
if (!dispatched) return errMsg("Geste konnte nicht ausgeloest werden ($desc)")
try { latch.await(6, TimeUnit.SECONDS) } catch (_: InterruptedException) {}
return if (ok.get()) okMsg("$desc ausgefuehrt") else errMsg("$desc abgebrochen/timeout")
}
/** Schreibt Text in ein Eingabefeld an (x,y) — oder in das fokussierte Feld. */
fun setText(x: Int, y: Int, text: String): JSONObject {
val root = rootInActiveWindow ?: return errMsg("kein aktives Fenster")
val node = editableAt(root, x, y)
?: root.findFocus(AccessibilityNodeInfo.FOCUS_INPUT)?.takeIf { it.isEditable }
?: return errMsg("kein Textfeld an ($x,$y) gefunden — vorher ui_tap aufs Feld?")
node.performAction(AccessibilityNodeInfo.ACTION_FOCUS)
val args = Bundle().apply {
putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text)
}
val done = node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args)
return if (done) okMsg("Text gesetzt (${text.length} Zeichen)")
else errMsg("Text setzen fehlgeschlagen (Feld nicht editierbar?)")
}
/** Tiefste editierbare Node, deren Rahmen (x,y) enthaelt. */
private fun editableAt(node: AccessibilityNodeInfo?, x: Int, y: Int): AccessibilityNodeInfo? {
if (node == null) return null
var found: AccessibilityNodeInfo? = null
for (i in 0 until node.childCount) {
editableAt(node.getChild(i), x, y)?.let { found = it }
}
if (found != null) return found
val r = Rect(); node.getBoundsInScreen(r)
return if (node.isEditable && r.contains(x, y)) node else null
}
/** Globale Taste: back/home/recents/notifications. */
fun globalKey(name: String): JSONObject {
val action = when (name.trim().lowercase()) {
"back", "zurueck", "zurück" -> GLOBAL_ACTION_BACK
"home", "start", "startseite" -> GLOBAL_ACTION_HOME
"recents", "letzte", "uebersicht", "übersicht" -> GLOBAL_ACTION_RECENTS
"notifications", "benachrichtigungen" -> GLOBAL_ACTION_NOTIFICATIONS
else -> return errMsg("Taste '$name' unbekannt (back/home/recents/notifications)")
}
return if (performGlobalAction(action)) okMsg("Taste '$name' ausgefuehrt")
else errMsg("Taste '$name' fehlgeschlagen")
}
private fun okMsg(m: String): JSONObject =
JSONObject().put("ok", true).put("result", JSONObject().put("message", m))
private fun errMsg(m: String): JSONObject =
JSONObject().put("ok", false).put("error", m)
companion object { companion object {
@Volatile @Volatile
var instance: AriaAccessibilityService? = null var instance: AriaAccessibilityService? = null
@@ -33,7 +33,10 @@ class RvsClient(
@Volatile private var running = false @Volatile private var running = false
private var pingThread: Thread? = null private var pingThread: Thread? = null
private val caps = listOf("info", "screenshot", "ui_dump") // M3: ui_tap/ui_text/… private val caps = listOf(
"info", "screenshot", "ui_dump",
"ui_tap", "ui_text", "ui_swipe", "ui_key", "app_launch",
)
fun start() { fun start() {
running = true running = true
@@ -126,6 +129,7 @@ class RvsClient(
&& !target.equals(config.displayName(), true)) return && !target.equals(config.displayName(), true)) return
val action = payload.optString("action") val action = payload.optString("action")
val params = payload.optJSONObject("params") ?: JSONObject()
// WICHTIG: JEDE Aktion muss ein host_result liefern — auch bei Exception // WICHTIG: JEDE Aktion muss ein host_result liefern — auch bei Exception
// ODER OutOfMemoryError (Throwable!). Sonst bekommt ARIA statt einer // ODER OutOfMemoryError (Throwable!). Sonst bekommt ARIA statt einer
// Fehlermeldung nur einen Timeout (kein Result kommt zurueck). // Fehlermeldung nur einen Timeout (kein Result kommt zurueck).
@@ -136,9 +140,13 @@ class RvsClient(
action == "info" -> doInfo() action == "info" -> doInfo()
action == "screenshot" -> doScreenshot() action == "screenshot" -> doScreenshot()
action == "ui_dump" -> doUiDump() action == "ui_dump" -> doUiDump()
action in listOf("ui_tap", "ui_text", "ui_swipe", "ui_key", action == "ui_tap" -> doUiTap(params)
"app_launch", "app_list", "notify") -> action == "ui_text" -> doUiText(params)
err("Aktion '$action' kommt in Meilenstein 3 (noch nicht implementiert).") action == "ui_swipe" -> doUiSwipe(params)
action == "ui_key" -> doUiKey(params)
action == "app_launch" -> doAppLaunch(params)
action in listOf("app_list", "notify") ->
err("Aktion '$action' kommt spaeter (noch nicht implementiert).")
else -> err("Aktion '$action' unbekannt.") else -> err("Aktion '$action' unbekannt.")
} }
} catch (t: Throwable) { } catch (t: Throwable) {
@@ -168,12 +176,75 @@ class RvsClient(
/** Sichtbare Bedienelemente als Baum (Bedienungshilfe). */ /** Sichtbare Bedienelemente als Baum (Bedienungshilfe). */
private fun doUiDump(): JSONObject { private fun doUiDump(): JSONObject {
val svc = AriaAccessibilityService.instance val svc = a11y() ?: return a11yMissing()
?: return err("Bedienungshilfe nicht aktiv. In der Agent-App 'Bedienungshilfe " +
"öffnen' antippen und 'ARIA Host-Agent' einschalten.")
return svc.dump() return svc.dump()
} }
private fun a11y(): AriaAccessibilityService? = AriaAccessibilityService.instance
private fun a11yMissing(): JSONObject =
err("Bedienungshilfe nicht aktiv. In der Agent-App 'Bedienungshilfe öffnen' " +
"antippen und 'ARIA Host-Agent' einschalten.")
/** Tippen auf Koordinaten (Pixel wie in ui_dump x/y). */
private fun doUiTap(p: JSONObject): JSONObject {
val svc = a11y() ?: return a11yMissing()
if (!p.has("x") || !p.has("y")) return err("ui_tap braucht x und y (aus ui_dump).")
return svc.tap(p.optInt("x"), p.optInt("y"))
}
/** Text in Feld an (x,y) schreiben. */
private fun doUiText(p: JSONObject): JSONObject {
val svc = a11y() ?: return a11yMissing()
val text = p.optString("text")
if (!p.has("x") || !p.has("y")) return err("ui_text braucht x, y und text.")
return svc.setText(p.optInt("x"), p.optInt("y"), text)
}
/** Wischen/Scrollen von (x1,y1) nach (x2,y2). */
private fun doUiSwipe(p: JSONObject): JSONObject {
val svc = a11y() ?: return a11yMissing()
if (!p.has("x1") || !p.has("y1") || !p.has("x2") || !p.has("y2"))
return err("ui_swipe braucht x1,y1,x2,y2 (optional duration_ms).")
return svc.swipe(p.optInt("x1"), p.optInt("y1"), p.optInt("x2"), p.optInt("y2"),
p.optInt("duration_ms", 300))
}
/** Globale Taste: back/home/recents/notifications. */
private fun doUiKey(p: JSONObject): JSONObject {
val svc = a11y() ?: return a11yMissing()
return svc.globalKey(p.optString("key"))
}
/** App starten (per Paketname oder Namens-Suche). */
private fun doAppLaunch(p: JSONObject): JSONObject {
val pm = appCtx.packageManager
var pkg = p.optString("package").trim()
val query = p.optString("query").trim()
if (pkg.isBlank() && query.isNotBlank()) {
pkg = resolvePackage(query) ?: return err("Keine App zu '$query' gefunden.")
}
if (pkg.isBlank()) return err("app_launch braucht 'package' ODER 'query' (App-Name).")
val intent = pm.getLaunchIntentForPackage(pkg)
?: return err("App '$pkg' nicht installiert oder ohne Start-Symbol.")
intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
appCtx.startActivity(intent)
return JSONObject().put("ok", true)
.put("result", JSONObject().put("message", "App '$pkg' gestartet"))
}
/** Paketname per Label-Teilstring finden (case-insensitive). */
private fun resolvePackage(query: String): String? {
val pm = appCtx.packageManager
val q = query.lowercase()
val launch = android.content.Intent(android.content.Intent.ACTION_MAIN)
.addCategory(android.content.Intent.CATEGORY_LAUNCHER)
return pm.queryIntentActivities(launch, 0)
.mapNotNull { it.activityInfo }
.firstOrNull { pm.getApplicationLabel(it.applicationInfo).toString().lowercase().contains(q) }
?.packageName
}
private fun doInfo(): JSONObject { private fun doInfo(): JSONObject {
val res = JSONObject() val res = JSONObject()
res.put("host", config.displayName()) res.put("host", config.displayName())
@@ -4,5 +4,6 @@
android:accessibilityFeedbackType="feedbackGeneric" android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagRetrieveInteractiveWindows|flagReportViewIds" android:accessibilityFlags="flagRetrieveInteractiveWindows|flagReportViewIds"
android:canRetrieveWindowContent="true" android:canRetrieveWindowContent="true"
android:canPerformGestures="true"
android:notificationTimeout="100" android:notificationTimeout="100"
android:description="@string/accessibility_desc" /> android:description="@string/accessibility_desc" />
+1 -1
View File
@@ -162,7 +162,7 @@ OUT_MAX_CHARS_HARD = int(os.environ.get("OUT_MAX_CHARS_HARD", "200000") or "2000
FILE_MAX_BYTES = int(os.environ.get("FILE_MAX_BYTES", str(10 * 1024 * 1024)) or str(10 * 1024 * 1024)) FILE_MAX_BYTES = int(os.environ.get("FILE_MAX_BYTES", str(10 * 1024 * 1024)) or str(10 * 1024 * 1024))
# Version (wird von release_agent.sh beim Release gesetzt). # Version (wird von release_agent.sh beim Release gesetzt).
AGENT_VERSION = "0.0.0.4" AGENT_VERSION = "0.0.0.6"
HEARTBEAT_SEC = 25 HEARTBEAT_SEC = 25
CAPS = ["exec", "read", "write", "info", "screenshot"] CAPS = ["exec", "read", "write", "info", "screenshot"]