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:
@@ -2,13 +2,14 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<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.software.leanback" android:required="false" />
|
||||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@drawable/ic_launcher_art"
|
android:icon="@mipmap/ic_launcher_art"
|
||||||
android:banner="@drawable/banner_tv_art"
|
android:banner="@mipmap/banner_tv_art"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
|
android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
package de.ggb.kwabatv
|
package de.ggb.kwabatv
|
||||||
|
|
||||||
import android.app.Activity
|
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.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
|
import android.provider.Settings
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.webkit.PermissionRequest
|
import android.webkit.PermissionRequest
|
||||||
@@ -49,6 +57,8 @@ class MainActivity : Activity() {
|
|||||||
private var fullscreenView: View? = null
|
private var fullscreenView: View? = null
|
||||||
private var currentPath: String = "/tv"
|
private var currentPath: String = "/tv"
|
||||||
private var lastThrottledSeekAt = 0L
|
private var lastThrottledSeekAt = 0L
|
||||||
|
private var pendingDownloadId: Long = -1
|
||||||
|
private var receiverRegistered = false
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -84,13 +94,92 @@ class MainActivity : Activity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
webView.loadUrl(BuildConfig.WEBSITE_URL)
|
webView.loadUrl(BuildConfig.WEBSITE_URL)
|
||||||
|
checkForUpdateInBackground()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
webView.destroy()
|
webView.destroy()
|
||||||
|
if (receiverRegistered) { unregisterReceiver(downloadReceiver); receiverRegistered = false }
|
||||||
super.onDestroy()
|
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 isRoomList() = currentPath == "/tv"
|
||||||
private fun isInRoom() = currentPath.startsWith("/tv/")
|
private fun isInRoom() = currentPath.startsWith("/tv/")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user