diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 464a705..945585d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,13 +2,14 @@ + diff --git a/app/src/main/java/de/ggb/kwabatv/MainActivity.kt b/app/src/main/java/de/ggb/kwabatv/MainActivity.kt index ccd6d93..b31e5e5 100644 --- a/app/src/main/java/de/ggb/kwabatv/MainActivity.kt +++ b/app/src/main/java/de/ggb/kwabatv/MainActivity.kt @@ -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/")