Fix Android TV APK distribution (#139)

* Fix Android TV APK distribution

* Fix Android TV Gradle settings

* Run Android TV APK checks on branch pushes

* Add Android TV launcher assets
This commit is contained in:
Kuek Hao Yang
2026-04-02 21:03:02 +08:00
committed by GitHub
parent f096cc977b
commit 644cc3993b
18 changed files with 739 additions and 21 deletions
@@ -1,6 +1,8 @@
package com.kvideo.tv
import android.annotation.SuppressLint
import android.content.Context
import android.net.Uri
import android.os.Bundle
import android.view.KeyEvent
import android.view.View
@@ -9,23 +11,30 @@ import android.webkit.WebChromeClient
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import androidx.activity.ComponentActivity
class MainActivity : ComponentActivity() {
companion object {
/**
* The URL of your deployed KVideo instance.
* Change this to your own domain or IP address.
*/
private const val KVIDEO_URL = "https://kvideo.example.com"
private const val PREFS_NAME = "kvideo_tv_settings"
private const val PREF_SERVER_URL = "server_url"
}
private lateinit var webView: WebView
private lateinit var setupContainer: View
private lateinit var urlInput: EditText
private lateinit var statusText: TextView
private lateinit var openButton: Button
private lateinit var saveButton: Button
private lateinit var prefs: android.content.SharedPreferences
@SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
// Fullscreen immersive mode
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
@@ -41,6 +50,15 @@ class MainActivity : ComponentActivity() {
setContentView(R.layout.activity_main)
webView = findViewById(R.id.webview)
setupContainer = findViewById(R.id.setup_container)
urlInput = findViewById(R.id.url_input)
statusText = findViewById(R.id.status_text)
openButton = findViewById(R.id.open_button)
saveButton = findViewById(R.id.save_button)
saveButton.setOnClickListener {
openConfiguredUrl()
}
webView.apply {
setLayerType(View.LAYER_TYPE_HARDWARE, null)
@@ -58,14 +76,25 @@ class MainActivity : ComponentActivity() {
webViewClient = WebViewClient()
webChromeClient = WebChromeClient()
}
loadUrl(KVIDEO_URL)
val configuredUrl = getConfiguredUrl()
if (configuredUrl.isNotEmpty()) {
urlInput.setText(configuredUrl)
loadConfiguredUrl(configuredUrl)
} else {
showSetup(getString(R.string.status_first_launch))
}
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
if (!isSetupVisible() && (keyCode == KeyEvent.KEYCODE_MENU || keyCode == KeyEvent.KEYCODE_SETTINGS)) {
showSetup(getString(R.string.status_settings_hint))
return true
}
// Map D-pad center to Enter for spatial navigation
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
if (!isSetupVisible() && keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
webView.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER))
return true
}
@@ -73,7 +102,7 @@ class MainActivity : ComponentActivity() {
}
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
if (!isSetupVisible() && keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
webView.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER))
return true
}
@@ -82,11 +111,16 @@ class MainActivity : ComponentActivity() {
@Deprecated("Use OnBackPressedDispatcher")
override fun onBackPressed() {
if (isSetupVisible()) {
@Suppress("DEPRECATION")
super.onBackPressed()
return
}
if (webView.canGoBack()) {
webView.goBack()
} else {
@Suppress("DEPRECATION")
super.onBackPressed()
showSetup(getString(R.string.status_settings_hint))
}
}
@@ -94,4 +128,86 @@ class MainActivity : ComponentActivity() {
webView.destroy()
super.onDestroy()
}
private fun openConfiguredUrl() {
val normalizedUrl = normalizeUrl(urlInput.text.toString())
if (!isValidUrl(normalizedUrl)) {
statusText.text = getString(R.string.status_invalid_url)
urlInput.requestFocus()
return
}
prefs.edit().putString(PREF_SERVER_URL, normalizedUrl).apply()
loadConfiguredUrl(normalizedUrl)
}
private fun loadConfiguredUrl(url: String) {
setupContainer.visibility = View.GONE
statusText.text = getString(R.string.status_ready)
webView.loadUrl(url)
}
private fun showSetup(message: String) {
val currentUrl = getConfiguredUrl()
if (currentUrl.isNotEmpty() && urlInput.text.toString().isBlank()) {
urlInput.setText(currentUrl)
}
if (currentUrl.isNotEmpty()) {
openButton.text = getString(R.string.button_open_saved)
openButton.setOnClickListener {
urlInput.setText(currentUrl)
loadConfiguredUrl(currentUrl)
}
} else {
openButton.text = getString(R.string.button_exit)
openButton.setOnClickListener {
finish()
}
}
statusText.text = message
setupContainer.visibility = View.VISIBLE
urlInput.requestFocus()
}
private fun getConfiguredUrl(): String {
val savedUrl = prefs.getString(PREF_SERVER_URL, null)?.trim().orEmpty()
if (isValidUrl(savedUrl)) {
return savedUrl
}
val defaultUrl = normalizeUrl(BuildConfig.DEFAULT_KVIDEO_URL)
return if (isValidUrl(defaultUrl)) defaultUrl else ""
}
private fun isSetupVisible(): Boolean = setupContainer.visibility == View.VISIBLE
private fun normalizeUrl(rawUrl: String): String {
val trimmed = rawUrl.trim()
if (trimmed.isEmpty()) {
return ""
}
val withScheme = if (
trimmed.startsWith("http://", ignoreCase = true) ||
trimmed.startsWith("https://", ignoreCase = true)
) {
trimmed
} else {
"https://$trimmed"
}
return withScheme.removeSuffix("/")
}
private fun isValidUrl(url: String): Boolean {
if (url.isBlank()) {
return false
}
val uri = Uri.parse(url)
val scheme = uri.scheme?.lowercase()
return (scheme == "http" || scheme == "https") && !uri.host.isNullOrBlank()
}
}