diff --git a/host-agent/android/app/build.gradle b/host-agent/android/app/build.gradle
index dc61aeb..e25fbee 100644
--- a/host-agent/android/app/build.gradle
+++ b/host-agent/android/app/build.gradle
@@ -11,8 +11,8 @@ android {
applicationId 'de.hackersoft.ariaagent'
minSdk 26
targetSdk 33 // 33 vermeidet die Foreground-Service-Typ-Pflicht von 34
- versionCode 7
- versionName '0.0.0.7'
+ versionCode 8
+ versionName '0.0.0.8'
}
// Fester Signaturschlüssel: jeder Build signiert mit DEMSELBEN Key, damit
diff --git a/host-agent/android/app/src/main/AndroidManifest.xml b/host-agent/android/app/src/main/AndroidManifest.xml
index 8ebd8bb..6a206dc 100644
--- a/host-agent/android/app/src/main/AndroidManifest.xml
+++ b/host-agent/android/app/src/main/AndroidManifest.xml
@@ -12,10 +12,17 @@
+
+
+
+
+
+
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
index f344868..7f69884 100644
--- 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
@@ -5,6 +5,7 @@ import android.accessibilityservice.GestureDescription
import android.graphics.Path
import android.graphics.Rect
import android.os.Bundle
+import android.view.View
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import org.json.JSONArray
@@ -148,19 +149,76 @@ class AriaAccessibilityService : AccessibilityService() {
return if (node.isEditable && r.contains(x, y)) node else null
}
- /** Globale Taste: back/home/recents/notifications. */
+ /** Globale Taste: back/home/recents/notifications — plus D-Pad (Fire TV). */
fun globalKey(name: String): JSONObject {
- val action = when (name.trim().lowercase()) {
+ val key = name.trim().lowercase()
+ // D-Pad/Medien: auf dem Fernseher wird damit navigiert (kein Touch).
+ DPAD_KEYS[key]?.let { return dpad(it, key) }
+ val action = when (key) {
"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)")
+ else -> return errMsg("Taste '$name' unbekannt. Verfuegbar: back, home, " +
+ "recents, notifications, up, down, left, right, ok/center, " +
+ "play_pause, next, prev, volume_up, volume_down, mute")
}
return if (performGlobalAction(action)) okMsg("Taste '$name' ausgefuehrt")
else errMsg("Taste '$name' fehlgeschlagen")
}
+ /** D-Pad/Medien-Taste — via Fokus-Navigation bzw. Medien-Session. */
+ private fun dpad(keyCode: Int, label: String): JSONObject {
+ // Lautstaerke/Stumm gehen ueber den AudioManager (zuverlaessiger als KeyEvent).
+ when (keyCode) {
+ android.view.KeyEvent.KEYCODE_VOLUME_UP,
+ android.view.KeyEvent.KEYCODE_VOLUME_DOWN,
+ android.view.KeyEvent.KEYCODE_VOLUME_MUTE -> {
+ val am = getSystemService(android.content.Context.AUDIO_SERVICE)
+ as android.media.AudioManager
+ val dir = when (keyCode) {
+ android.view.KeyEvent.KEYCODE_VOLUME_UP -> android.media.AudioManager.ADJUST_RAISE
+ android.view.KeyEvent.KEYCODE_VOLUME_DOWN -> android.media.AudioManager.ADJUST_LOWER
+ else -> android.media.AudioManager.ADJUST_TOGGLE_MUTE
+ }
+ am.adjustStreamVolume(android.media.AudioManager.STREAM_MUSIC, dir,
+ android.media.AudioManager.FLAG_SHOW_UI)
+ return okMsg("Taste '$label' ausgefuehrt")
+ }
+ }
+ // Medientasten an die aktive Wiedergabe-Session.
+ if (keyCode in MEDIA_KEYS) {
+ val am = getSystemService(android.content.Context.AUDIO_SERVICE)
+ as android.media.AudioManager
+ val now = android.os.SystemClock.uptimeMillis()
+ am.dispatchMediaKeyEvent(android.view.KeyEvent(now, now,
+ android.view.KeyEvent.ACTION_DOWN, keyCode, 0))
+ am.dispatchMediaKeyEvent(android.view.KeyEvent(now, now,
+ android.view.KeyEvent.ACTION_UP, keyCode, 0))
+ return okMsg("Taste '$label' ausgefuehrt")
+ }
+ // Richtungen/OK: ueber die Bedienungshilfe-Fokus-Navigation.
+ val root = rootInActiveWindow ?: return errMsg("kein aktives Fenster")
+ val focus = root.findFocus(AccessibilityNodeInfo.FOCUS_INPUT)
+ ?: root.findFocus(AccessibilityNodeInfo.FOCUS_ACCESSIBILITY)
+ if (keyCode == android.view.KeyEvent.KEYCODE_DPAD_CENTER) {
+ val target = focus ?: return errMsg("nichts fokussiert — vorher navigieren")
+ return if (target.performAction(AccessibilityNodeInfo.ACTION_CLICK))
+ okMsg("OK/Auswahl ausgefuehrt") else errMsg("Auswahl fehlgeschlagen")
+ }
+ val dir = when (keyCode) {
+ android.view.KeyEvent.KEYCODE_DPAD_UP -> View.FOCUS_UP
+ android.view.KeyEvent.KEYCODE_DPAD_DOWN -> View.FOCUS_DOWN
+ android.view.KeyEvent.KEYCODE_DPAD_LEFT -> View.FOCUS_LEFT
+ else -> View.FOCUS_RIGHT
+ }
+ val from = focus ?: root
+ val next = from.focusSearch(dir)
+ ?: return errMsg("kein Element in Richtung '$label' gefunden")
+ return if (next.performAction(AccessibilityNodeInfo.ACTION_FOCUS))
+ okMsg("Taste '$label' ausgefuehrt") else errMsg("Taste '$label' fehlgeschlagen")
+ }
+
private fun okMsg(m: String): JSONObject =
JSONObject().put("ok", true).put("result", JSONObject().put("message", m))
@@ -172,5 +230,46 @@ class AriaAccessibilityService : AccessibilityService() {
var instance: AriaAccessibilityService? = null
val available: Boolean get() = instance != null
+
+ /** Fernseher-Navigation (Fire TV): D-Pad, Medien- und Lautstaerke-Tasten. */
+ private val DPAD_KEYS: Map = mapOf(
+ "up" to android.view.KeyEvent.KEYCODE_DPAD_UP,
+ "hoch" to android.view.KeyEvent.KEYCODE_DPAD_UP,
+ "down" to android.view.KeyEvent.KEYCODE_DPAD_DOWN,
+ "runter" to android.view.KeyEvent.KEYCODE_DPAD_DOWN,
+ "left" to android.view.KeyEvent.KEYCODE_DPAD_LEFT,
+ "links" to android.view.KeyEvent.KEYCODE_DPAD_LEFT,
+ "right" to android.view.KeyEvent.KEYCODE_DPAD_RIGHT,
+ "rechts" to android.view.KeyEvent.KEYCODE_DPAD_RIGHT,
+ "ok" to android.view.KeyEvent.KEYCODE_DPAD_CENTER,
+ "center" to android.view.KeyEvent.KEYCODE_DPAD_CENTER,
+ "select" to android.view.KeyEvent.KEYCODE_DPAD_CENTER,
+ "enter" to android.view.KeyEvent.KEYCODE_DPAD_CENTER,
+ "play_pause" to android.view.KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE,
+ "play" to android.view.KeyEvent.KEYCODE_MEDIA_PLAY,
+ "pause" to android.view.KeyEvent.KEYCODE_MEDIA_PAUSE,
+ "stop" to android.view.KeyEvent.KEYCODE_MEDIA_STOP,
+ "next" to android.view.KeyEvent.KEYCODE_MEDIA_NEXT,
+ "prev" to android.view.KeyEvent.KEYCODE_MEDIA_PREVIOUS,
+ "forward" to android.view.KeyEvent.KEYCODE_MEDIA_FAST_FORWARD,
+ "rewind" to android.view.KeyEvent.KEYCODE_MEDIA_REWIND,
+ "volume_up" to android.view.KeyEvent.KEYCODE_VOLUME_UP,
+ "lauter" to android.view.KeyEvent.KEYCODE_VOLUME_UP,
+ "volume_down" to android.view.KeyEvent.KEYCODE_VOLUME_DOWN,
+ "leiser" to android.view.KeyEvent.KEYCODE_VOLUME_DOWN,
+ "mute" to android.view.KeyEvent.KEYCODE_VOLUME_MUTE,
+ "stumm" to android.view.KeyEvent.KEYCODE_VOLUME_MUTE,
+ )
+
+ private val MEDIA_KEYS: Set = setOf(
+ android.view.KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE,
+ android.view.KeyEvent.KEYCODE_MEDIA_PLAY,
+ android.view.KeyEvent.KEYCODE_MEDIA_PAUSE,
+ android.view.KeyEvent.KEYCODE_MEDIA_STOP,
+ android.view.KeyEvent.KEYCODE_MEDIA_NEXT,
+ android.view.KeyEvent.KEYCODE_MEDIA_PREVIOUS,
+ android.view.KeyEvent.KEYCODE_MEDIA_FAST_FORWARD,
+ android.view.KeyEvent.KEYCODE_MEDIA_REWIND,
+ )
}
}
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 a33e54c..64fdea8 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
@@ -101,6 +101,13 @@ class MainActivity : AppCompatActivity() {
loadIntoUi(AgentConfig.load(this))
+ // Fire TV: kein Touchscreen — der QR-Scan ist dort sinnlos (keine Kamera),
+ // und der erste Fokus muss gesetzt sein, damit die Fernbedienung greift.
+ if (isTvDevice()) {
+ findViewById