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
|
||||
|
||||
import android.app.Activity
|
||||
import android.net.Uri
|
||||
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.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() {
|
||||
|
||||
private lateinit var webView: WebView
|
||||
private var fullscreenView: View? = null
|
||||
private var currentPath: String = "/tv"
|
||||
private var lastThrottledSeekAt = 0L
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val webView = WebView(this)
|
||||
|
||||
webView = WebView(this)
|
||||
setContentView(webView)
|
||||
|
||||
webView.settings.javaScriptEnabled = 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)
|
||||
}
|
||||
|
||||
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