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