Add remote-control key mapping, fullscreen tracking, back-button navigation
dispatchKeyEvent() intercepts DPAD/media keys and re-dispatches real KeyEvents into the WebView per the design spec's mapping table: - DPAD center / Play-Pause -> Space - DPAD Left/Right tap -> Arrow (5s seek); held -> Shift+Arrow (30s seek), throttled to ~1 event/200ms since Android's own key-repeat rate would otherwise flood the room's sync socket - DPAD Up/Down + media Prev/Next -> P/N (title change, not ArrowUp/Down - those are explicit no-ops in the website's TV mode since volume is TV/AVR-controlled) - Back: room list -> no-op; in a room + fullscreen -> Escape (browser exits fullscreen natively); in a room + not fullscreen -> navigate to /tv (native WebView navigation, not a website reaction) - Menu (KEYCODE_MENU) deliberately NOT intercepted - passed through unchanged, matching the already-confirmed-working raw-browser test Fullscreen state comes from WebChromeClient.onShowCustomView/ onHideCustomView (needed for HTML5 video fullscreen support anyway). Current-page tracking comes from WebViewClient.onPageFinished. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LHgWjmEJXsJEi1EVAoTp4K
This commit is contained in:
@@ -1,16 +1,144 @@
|
|||||||
package de.ggb.kwabatv
|
package de.ggb.kwabatv
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.SystemClock
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.webkit.PermissionRequest
|
||||||
|
import android.webkit.WebChromeClient
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
|
import android.webkit.WebViewClient
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reine WebView-Huelle. Fernbedienungstasten werden in dispatchKeyEvent()
|
||||||
|
* abgefangen und als echte (nicht per JS simulierte) KeyEvents an das
|
||||||
|
* WebView weitergereicht - Chromium verlangt fuer die Fullscreen-API und
|
||||||
|
* den einmaligen Autoplay-Ton-Klick eine vertrauenswuerdige Nutzereingabe,
|
||||||
|
* die ein simuliertes KeyboardEvent nicht liefert.
|
||||||
|
*
|
||||||
|
* KEYCODE_MENU wird bewusst NICHT abgefangen: das Oeffnen des TV-Menue-
|
||||||
|
* Popups wurde bereits gegen die echte Website in einem normalen Browser
|
||||||
|
* auf echter Fire-TV-Hardware bestaetigt, ohne dass diese App beteiligt
|
||||||
|
* war - unveraendertes Durchreichen ist die naechste Annaeherung an genau
|
||||||
|
* dieses schon erprobte Verhalten.
|
||||||
|
*/
|
||||||
class MainActivity : Activity() {
|
class MainActivity : Activity() {
|
||||||
|
|
||||||
|
private lateinit var webView: WebView
|
||||||
|
private var fullscreenView: View? = null
|
||||||
|
private var currentPath: String = "/tv"
|
||||||
|
private var lastThrottledSeekAt = 0L
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
val webView = WebView(this)
|
|
||||||
|
webView = WebView(this)
|
||||||
setContentView(webView)
|
setContentView(webView)
|
||||||
|
|
||||||
webView.settings.javaScriptEnabled = true
|
webView.settings.javaScriptEnabled = true
|
||||||
webView.settings.domStorageEnabled = true
|
webView.settings.domStorageEnabled = true
|
||||||
|
webView.settings.mediaPlaybackRequiresUserGesture = false
|
||||||
|
|
||||||
|
webView.webViewClient = object : WebViewClient() {
|
||||||
|
override fun onPageFinished(view: WebView, url: String) {
|
||||||
|
super.onPageFinished(view, url)
|
||||||
|
currentPath = Uri.parse(url).path ?: "/tv"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
webView.webChromeClient = object : WebChromeClient() {
|
||||||
|
override fun onShowCustomView(view: View, callback: CustomViewCallback) {
|
||||||
|
fullscreenView = view
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onHideCustomView() {
|
||||||
|
fullscreenView = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPermissionRequest(request: PermissionRequest) {
|
||||||
|
// Kein Mikrofon/Kamera im TV-Modus - Fire-TV-Sticks haben
|
||||||
|
// ohnehin meist keine Kamera, siehe Fixed Product Decisions.
|
||||||
|
request.deny()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
webView.loadUrl(BuildConfig.WEBSITE_URL)
|
webView.loadUrl(BuildConfig.WEBSITE_URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
webView.destroy()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isFullscreen() = fullscreenView != null
|
||||||
|
private fun isRoomList() = currentPath == "/tv"
|
||||||
|
|
||||||
|
private fun sendKey(keyCode: Int, shift: Boolean = false) {
|
||||||
|
val metaState = if (shift) KeyEvent.META_SHIFT_ON else 0
|
||||||
|
val now = SystemClock.uptimeMillis()
|
||||||
|
webView.dispatchKeyEvent(KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyCode, 0, metaState))
|
||||||
|
webView.dispatchKeyEvent(KeyEvent(now, now, KeyEvent.ACTION_UP, keyCode, 0, metaState))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tippen = 5s-Schritt (kein Shift), gehalten = 30s-Schritt (mit Shift),
|
||||||
|
* gedrosselt auf ~1 Ereignis pro 200ms - Androids eigene Tastenwieder-
|
||||||
|
* holung feuert schneller als das und wuerde sonst den Sync-Socket des
|
||||||
|
* Raums mit Sprungbefehlen ueberfluten, die an alle Zuschauer gehen. */
|
||||||
|
private fun handleSeek(keyCode: Int, isRepeat: Boolean) {
|
||||||
|
if (!isRepeat) { sendKey(keyCode); return }
|
||||||
|
val now = SystemClock.uptimeMillis()
|
||||||
|
if (now - lastThrottledSeekAt < SEEK_THROTTLE_MS) return
|
||||||
|
lastThrottledSeekAt = now
|
||||||
|
sendKey(keyCode, shift = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||||
|
if (event.action != KeyEvent.ACTION_DOWN) {
|
||||||
|
return when (event.keyCode) {
|
||||||
|
KeyEvent.KEYCODE_DPAD_CENTER, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE,
|
||||||
|
KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_DPAD_RIGHT,
|
||||||
|
KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_DPAD_DOWN,
|
||||||
|
KeyEvent.KEYCODE_MEDIA_PREVIOUS, KeyEvent.KEYCODE_MEDIA_NEXT,
|
||||||
|
KeyEvent.KEYCODE_BACK -> true // ACTION_UP derselben Tasten verschlucken, wir haben schon auf ACTION_DOWN reagiert
|
||||||
|
else -> super.dispatchKeyEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return when (event.keyCode) {
|
||||||
|
KeyEvent.KEYCODE_DPAD_CENTER, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
|
||||||
|
sendKey(KeyEvent.KEYCODE_SPACE)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_DPAD_LEFT -> {
|
||||||
|
handleSeek(KeyEvent.KEYCODE_DPAD_LEFT, event.repeatCount > 0)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_DPAD_RIGHT -> {
|
||||||
|
handleSeek(KeyEvent.KEYCODE_DPAD_RIGHT, event.repeatCount > 0)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_MEDIA_PREVIOUS -> {
|
||||||
|
sendKey(KeyEvent.KEYCODE_P)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_DPAD_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT -> {
|
||||||
|
sendKey(KeyEvent.KEYCODE_N)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_BACK -> {
|
||||||
|
when {
|
||||||
|
isRoomList() -> { /* verschluckt, keine weitere Aktion */ }
|
||||||
|
isFullscreen() -> sendKey(KeyEvent.KEYCODE_ESCAPE)
|
||||||
|
else -> webView.loadUrl(BuildConfig.WEBSITE_URL)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> super.dispatchKeyEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val SEEK_THROTTLE_MS = 200L
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user