feat(android-agent): Meilenstein 2 — sehen (Screenshot + ui_dump)
Screenshot via MediaProjection (ScreenCapturer, gleicher {format,bytes,base64}-
Vertrag wie der Desktop-Agent -> host_screenshot, inkl. Vision). UI-Baum via
AriaAccessibilityService (nur lesend) -> neues Brain-Tool host_ui_dump. Freigabe
einmalig in der App: 'Bildschirm-Zugriff erlauben' + 'Bedienungshilfe oeffnen'.
caps = [info, screenshot, ui_dump]. targetSdk 33 -> keine mediaProjection-FGS-
Typpflicht.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+36
-2
@@ -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()
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -39,5 +39,18 @@
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<service
|
||||
android:name=".AriaAccessibilityService"
|
||||
android:exported="false"
|
||||
android:label="ARIA Host-Agent"
|
||||
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
|
||||
<intent-filter>
|
||||
<action android:name="android.accessibilityservice.AccessibilityService" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.accessibilityservice"
|
||||
android:resource="@xml/accessibility_config" />
|
||||
</service>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -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<Intent>(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))
|
||||
|
||||
+91
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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<Button>(R.id.btnScreen).setOnClickListener {
|
||||
if (!AgentService.connected && !AgentConfig.load(this).isValid()) {
|
||||
toast("Erst verbinden, dann Bildschirm-Zugriff erlauben")
|
||||
return@setOnClickListener
|
||||
}
|
||||
val mpm = getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
|
||||
projection.launch(mpm.createScreenCaptureIntent())
|
||||
}
|
||||
findViewById<Button>(R.id.btnAccessibility).setOnClickListener {
|
||||
try {
|
||||
startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))
|
||||
toast("'ARIA Host-Agent' in der Liste einschalten")
|
||||
} catch (_: Exception) {
|
||||
toast("Bedienungshilfe-Einstellungen nicht gefunden")
|
||||
}
|
||||
}
|
||||
|
||||
loadIntoUi(AgentConfig.load(this))
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ class RvsClient(
|
||||
@Volatile private var running = false
|
||||
private var pingThread: Thread? = null
|
||||
|
||||
private val caps = listOf("info") // waechst in M2/M3
|
||||
private val caps = listOf("info", "screenshot", "ui_dump") // M3: ui_tap/ui_text/…
|
||||
|
||||
fun start() {
|
||||
running = true
|
||||
@@ -130,9 +130,11 @@ class RvsClient(
|
||||
!config.controlEnabled ->
|
||||
err("Steuerung ist in der Agent-App deaktiviert (Schalter 'Steuerung erlauben').")
|
||||
action == "info" -> doInfo()
|
||||
action in listOf("screenshot", "ui_dump", "ui_tap", "ui_text",
|
||||
"ui_swipe", "ui_key", "app_launch", "app_list", "notify") ->
|
||||
err("Aktion '$action' kommt in Meilenstein 2/3 (noch nicht implementiert).")
|
||||
action == "screenshot" -> doScreenshot()
|
||||
action == "ui_dump" -> doUiDump()
|
||||
action in listOf("ui_tap", "ui_text", "ui_swipe", "ui_key",
|
||||
"app_launch", "app_list", "notify") ->
|
||||
err("Aktion '$action' kommt in Meilenstein 3 (noch nicht implementiert).")
|
||||
else -> err("Aktion '$action' unbekannt.")
|
||||
}
|
||||
result.put("requestId", payload.optString("requestId"))
|
||||
@@ -143,6 +145,26 @@ class RvsClient(
|
||||
|
||||
private fun err(m: String): JSONObject = JSONObject().put("ok", false).put("error", m)
|
||||
|
||||
/** Bildschirmfoto — selber Vertrag wie der Desktop-Agent: {format,bytes,base64}. */
|
||||
private fun doScreenshot(): JSONObject {
|
||||
if (!ScreenCapturer.active)
|
||||
return err("Bildschirm-Zugriff nicht erlaubt. In der Agent-App auf dem Handy " +
|
||||
"einmalig 'Bildschirm-Zugriff erlauben' antippen.")
|
||||
val png = ScreenCapturer.capture()
|
||||
?: return err("Screenshot fehlgeschlagen (kein Frame). Ist der Bildschirm an?")
|
||||
val b64 = android.util.Base64.encodeToString(png, android.util.Base64.NO_WRAP)
|
||||
val res = JSONObject().put("format", "png").put("bytes", png.size).put("base64", b64)
|
||||
return JSONObject().put("ok", true).put("result", res)
|
||||
}
|
||||
|
||||
/** Sichtbare Bedienelemente als Baum (Bedienungshilfe). */
|
||||
private fun doUiDump(): JSONObject {
|
||||
val svc = AriaAccessibilityService.instance
|
||||
?: return err("Bedienungshilfe nicht aktiv. In der Agent-App 'Bedienungshilfe " +
|
||||
"öffnen' antippen und 'ARIA Host-Agent' einschalten.")
|
||||
return svc.dump()
|
||||
}
|
||||
|
||||
private fun doInfo(): JSONObject {
|
||||
val res = JSONObject()
|
||||
res.put("host", config.displayName())
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
package de.hackersoft.ariaagent
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.PixelFormat
|
||||
import android.hardware.display.DisplayManager
|
||||
import android.hardware.display.VirtualDisplay
|
||||
import android.media.Image
|
||||
import android.media.ImageReader
|
||||
import android.media.projection.MediaProjection
|
||||
import android.media.projection.MediaProjectionManager
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.WindowManager
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
/**
|
||||
* Bildschirm-Aufnahme via MediaProjection (Meilenstein 2 — "sehen").
|
||||
*
|
||||
* Der Nutzer erlaubt den Zugriff EINMALIG in der Agent-App (System-Dialog).
|
||||
* Danach laeuft ein stiller VirtualDisplay -> ImageReader, aus dem `capture()`
|
||||
* bei Bedarf das aktuelle Bild als PNG zieht. Kein Google-Dienst.
|
||||
*
|
||||
* Der Zugriff geht bei App-Kill / Neustart verloren und muss neu erlaubt werden
|
||||
* (Android-Sicherheit — Projection-Token ist nicht persistierbar).
|
||||
*/
|
||||
object ScreenCapturer {
|
||||
private var projection: MediaProjection? = null
|
||||
private var reader: ImageReader? = null
|
||||
private var vdisplay: VirtualDisplay? = null
|
||||
private var handlerThread: HandlerThread? = null
|
||||
private var handler: Handler? = null
|
||||
private var w = 0
|
||||
private var h = 0
|
||||
private var dpi = 0
|
||||
|
||||
val active: Boolean
|
||||
@Synchronized get() = projection != null
|
||||
|
||||
@Synchronized
|
||||
fun start(ctx: Context, resultCode: Int, data: Intent) {
|
||||
stop()
|
||||
val mpm = ctx.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
|
||||
val mp = mpm.getMediaProjection(resultCode, data) ?: return
|
||||
|
||||
val metrics = DisplayMetrics()
|
||||
val wm = ctx.getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
||||
@Suppress("DEPRECATION")
|
||||
wm.defaultDisplay.getRealMetrics(metrics)
|
||||
w = metrics.widthPixels
|
||||
h = metrics.heightPixels
|
||||
dpi = metrics.densityDpi
|
||||
|
||||
handlerThread = HandlerThread("aria-capture").also { it.start() }
|
||||
handler = Handler(handlerThread!!.looper)
|
||||
|
||||
mp.registerCallback(object : MediaProjection.Callback() {
|
||||
override fun onStop() { stop() }
|
||||
}, handler)
|
||||
|
||||
val ir = ImageReader.newInstance(w, h, PixelFormat.RGBA_8888, 2)
|
||||
reader = ir
|
||||
vdisplay = mp.createVirtualDisplay(
|
||||
"aria-screen", w, h, dpi,
|
||||
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
|
||||
ir.surface, null, handler,
|
||||
)
|
||||
projection = mp
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun stop() {
|
||||
try { vdisplay?.release() } catch (_: Exception) {}
|
||||
try { reader?.close() } catch (_: Exception) {}
|
||||
try { projection?.stop() } catch (_: Exception) {}
|
||||
try { handlerThread?.quitSafely() } catch (_: Exception) {}
|
||||
vdisplay = null
|
||||
reader = null
|
||||
projection = null
|
||||
handlerThread = null
|
||||
handler = null
|
||||
}
|
||||
|
||||
/** PNG-Bytes des aktuellen Bildschirms, oder null. Wartet kurz auf einen Frame. */
|
||||
fun capture(): ByteArray? {
|
||||
val r = reader ?: return null
|
||||
var image: Image? = null
|
||||
var tries = 0
|
||||
while (tries < 20) {
|
||||
image = r.acquireLatestImage()
|
||||
if (image != null) break
|
||||
try { Thread.sleep(80) } catch (_: InterruptedException) {}
|
||||
tries++
|
||||
}
|
||||
if (image == null) return null
|
||||
return try {
|
||||
val plane = image.planes[0]
|
||||
val buffer = plane.buffer
|
||||
val pixelStride = plane.pixelStride
|
||||
val rowStride = plane.rowStride
|
||||
val rowPadding = rowStride - pixelStride * w
|
||||
val padded = Bitmap.createBitmap(
|
||||
w + (if (pixelStride > 0) rowPadding / pixelStride else 0),
|
||||
h, Bitmap.Config.ARGB_8888,
|
||||
)
|
||||
padded.copyPixelsFromBuffer(buffer)
|
||||
val cropped = if (rowPadding == 0) padded else Bitmap.createBitmap(padded, 0, 0, w, h)
|
||||
val scaled = downscale(cropped, 1280)
|
||||
val out = ByteArrayOutputStream()
|
||||
scaled.compress(Bitmap.CompressFormat.PNG, 100, out)
|
||||
if (scaled !== cropped) scaled.recycle()
|
||||
if (cropped !== padded) cropped.recycle()
|
||||
padded.recycle()
|
||||
out.toByteArray()
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
} finally {
|
||||
try { image?.close() } catch (_: Exception) {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun downscale(src: Bitmap, maxSide: Int): Bitmap {
|
||||
val longSide = maxOf(src.width, src.height)
|
||||
if (longSide <= maxSide) return src
|
||||
val scale = maxSide.toFloat() / longSide
|
||||
return Bitmap.createScaledBitmap(
|
||||
src, (src.width * scale).toInt(), (src.height * scale).toInt(), true,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -96,5 +96,33 @@
|
||||
android:text="Agent stoppen"
|
||||
android:paddingTop="8dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Sehen (Meilenstein 2)"
|
||||
android:textStyle="bold"
|
||||
android:paddingTop="24dp"
|
||||
android:paddingBottom="4dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Damit ARIA Screenshots machen und die Oberfläche lesen kann. Beides einmalig freigeben."
|
||||
android:textSize="13sp"
|
||||
android:paddingBottom="8dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnScreen"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Bildschirm-Zugriff erlauben" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnAccessibility"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Bedienungshilfe öffnen"
|
||||
android:paddingTop="8dp" />
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">ARIA Host-Agent</string>
|
||||
<string name="accessibility_desc">Erlaubt ARIA, die sichtbaren Bildschirm-Elemente zu lesen (Text und Position), um Dich fernzusteuern. Nur aktiv, wenn Du \'Steuerung erlauben\' eingeschaltet hast.</string>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged"
|
||||
android:accessibilityFeedbackType="feedbackGeneric"
|
||||
android:accessibilityFlags="flagRetrieveInteractiveWindows|flagReportViewIds"
|
||||
android:canRetrieveWindowContent="true"
|
||||
android:notificationTimeout="100"
|
||||
android:description="@string/accessibility_desc" />
|
||||
Reference in New Issue
Block a user