Wire update check into MainActivity: dialog, download, install

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LHgWjmEJXsJEi1EVAoTp4K
This commit is contained in:
GGB
2026-09-27 19:22:34 +02:00
co-authored by Claude Sonnet 5
parent d67c2a54de
commit 29065f8c29
2 changed files with 92 additions and 2 deletions
+3 -2
View File
@@ -2,13 +2,14 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-feature android:name="android.software.leanback" android:required="false" />
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher_art"
android:banner="@drawable/banner_tv_art"
android:icon="@mipmap/ic_launcher_art"
android:banner="@mipmap/banner_tv_art"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
@@ -1,9 +1,17 @@
package de.ggb.kwabatv
import android.app.Activity
import android.app.AlertDialog
import android.app.DownloadManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.SystemClock
import android.provider.Settings
import android.view.KeyEvent
import android.view.View
import android.webkit.PermissionRequest
@@ -49,6 +57,8 @@ class MainActivity : Activity() {
private var fullscreenView: View? = null
private var currentPath: String = "/tv"
private var lastThrottledSeekAt = 0L
private var pendingDownloadId: Long = -1
private var receiverRegistered = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -84,13 +94,92 @@ class MainActivity : Activity() {
}
webView.loadUrl(BuildConfig.WEBSITE_URL)
checkForUpdateInBackground()
}
override fun onDestroy() {
webView.destroy()
if (receiverRegistered) { unregisterReceiver(downloadReceiver); receiverRegistered = false }
super.onDestroy()
}
/* ---------------------------------------------------------- In-App-Update
Prueft bei jedem Start das oeffentliche KwabaTV-Gitea-Repo auf ein neueres
Release (siehe UpdateChecker.kt) und bietet es ueber Androids eigenen
Paket-Installer an - nie ohne Bestaetigung, das Betriebssystem selbst
erlaubt keine stille Installation. Ein fehlgeschlagener Check ist immer
lautlos: kein Fehlerdialog, einfach beim naechsten Start erneut versuchen. */
private fun checkForUpdateInBackground() {
Thread {
val update = UpdateChecker.checkForUpdate(BuildConfig.VERSION_NAME)
if (update != null) runOnUiThread { showUpdateDialog(update) }
}.start()
}
private fun showUpdateDialog(update: UpdateInfo) {
AlertDialog.Builder(this)
.setTitle("Update verfügbar")
.setMessage("KwabaTV v${update.version} ist verfügbar.")
.setPositiveButton("Aktualisieren") { _, _ -> startUpdate(update) }
.setNegativeButton("Später", null)
.show()
}
/* "Unbekannte Apps installieren" ist fuer neue Apps ab Android 8 (API 26)
pro App einzeln zu erlauben - ohne das wuerde der Installations-Intent
unten stillschweigend nichts tun. Statt automatisch nach der Rueckkehr
aus den Einstellungen weiterzumachen (mehr Zustand, mehr Fehlerquellen),
muss der Nutzer "Aktualisieren" nach dem Erlauben schlicht erneut
druecken - siehe Design-Spec. */
private fun startUpdate(update: UpdateInfo) {
val canInstall = Build.VERSION.SDK_INT < Build.VERSION_CODES.O || packageManager.canRequestPackageInstalls()
if (!canInstall) {
startActivity(Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.parse("package:$packageName")))
return
}
downloadApk(update.apkDownloadUrl)
}
private fun downloadApk(url: String) {
val request = DownloadManager.Request(Uri.parse(url))
.setTitle("KwabaTV Update")
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setDestinationInExternalFilesDir(this, null, "KwabaTV-update.apk")
val downloadManager = getSystemService(DOWNLOAD_SERVICE) as DownloadManager
pendingDownloadId = downloadManager.enqueue(request)
// Ab Android 13 (API 33) muss jeder dynamisch registrierte Receiver
// explizit als (nicht-)exportiert markiert sein, sonst wirft
// registerReceiver() eine SecurityException.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(downloadReceiver, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE), Context.RECEIVER_NOT_EXPORTED)
} else {
@Suppress("UnspecifiedRegisterReceiverFlag")
registerReceiver(downloadReceiver, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
}
receiverRegistered = true
}
private val downloadReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1) != pendingDownloadId) return
if (receiverRegistered) { unregisterReceiver(this); receiverRegistered = false }
val downloadManager = getSystemService(DOWNLOAD_SERVICE) as DownloadManager
// getUriForDownloadedFile() returns null for a failed download - that failure is
// silent for now too, matching the update check's own error handling (see spec).
val uri = downloadManager.getUriForDownloadedFile(pendingDownloadId) ?: return
installApk(uri)
}
}
private fun installApk(uri: Uri) {
val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(uri, "application/vnd.android.package-archive")
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
startActivity(intent)
}
private fun isRoomList() = currentPath == "/tv"
private fun isInRoom() = currentPath.startsWith("/tv/")