diff --git a/app-tv/src/main/AndroidManifest.xml b/app-tv/src/main/AndroidManifest.xml
index 61514307..a7354a47 100644
--- a/app-tv/src/main/AndroidManifest.xml
+++ b/app-tv/src/main/AndroidManifest.xml
@@ -1,10 +1,12 @@
+
+
+
+
diff --git a/app-tv/src/main/java/hu/bbara/purefin/tv/TvActivity.kt b/app-tv/src/main/java/hu/bbara/purefin/tv/TvActivity.kt
index 835a7daa..832855f1 100644
--- a/app-tv/src/main/java/hu/bbara/purefin/tv/TvActivity.kt
+++ b/app-tv/src/main/java/hu/bbara/purefin/tv/TvActivity.kt
@@ -2,6 +2,7 @@ package hu.bbara.purefin.tv
import android.content.pm.ApplicationInfo
import android.os.Bundle
+import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
@@ -11,6 +12,9 @@ import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
@@ -20,6 +24,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import androidx.navigation3.runtime.EntryProviderScope
@@ -38,10 +44,12 @@ import coil3.util.DebugLogger
import dagger.hilt.android.AndroidEntryPoint
import hu.bbara.purefin.data.SessionBootstrapper
import hu.bbara.purefin.data.UserSessionRepository
+import hu.bbara.purefin.feature.update.AppUpdateInfo
+import hu.bbara.purefin.feature.update.AppUpdateViewModel
+import hu.bbara.purefin.jellyfin.JellyfinAuthInterceptor
import hu.bbara.purefin.navigation.NavigationCommand
import hu.bbara.purefin.navigation.NavigationManager
import hu.bbara.purefin.navigation.Route
-import hu.bbara.purefin.jellyfin.JellyfinAuthInterceptor
import hu.bbara.purefin.navigation.LocalNavigationBackStack
import hu.bbara.purefin.navigation.LocalNavigationManager
import hu.bbara.purefin.ui.screen.login.LoginScreen
@@ -135,10 +143,13 @@ class TvActivity : ComponentActivity() {
fun MainApp(
userSessionRepository: UserSessionRepository,
entryBuilders: Set<@JvmSuppressWildcards EntryProviderScope.() -> Unit>,
- navigationManager: NavigationManager
+ navigationManager: NavigationManager,
+ updateViewModel: AppUpdateViewModel = hiltViewModel()
) {
var sessionLoaded by remember { mutableStateOf(false) }
val isLoggedIn by userSessionRepository.isLoggedIn.collectAsState(initial = false)
+ val availableUpdate by updateViewModel.availableUpdate.collectAsState()
+ val context = LocalContext.current
LaunchedEffect(Unit) {
userSessionRepository.isLoggedIn.collect {
@@ -146,12 +157,19 @@ class TvActivity : ComponentActivity() {
}
}
- if (!sessionLoaded) {
- PurefinWaitingScreen(modifier = Modifier.fillMaxSize())
- return
+ LaunchedEffect(updateViewModel) {
+ updateViewModel.checkForUpdates(showUpToDateMessage = false)
}
- if (isLoggedIn) {
+ LaunchedEffect(updateViewModel, context) {
+ updateViewModel.snackbarMessages.collect { message ->
+ Toast.makeText(context, message, Toast.LENGTH_LONG).show()
+ }
+ }
+
+ if (!sessionLoaded) {
+ PurefinWaitingScreen(modifier = Modifier.fillMaxSize())
+ } else if (isLoggedIn) {
@Suppress("UNCHECKED_CAST")
val backStack = rememberNavBackStack(Route.Home) as NavBackStack
val appEntryProvider = entryProvider {
@@ -213,5 +231,44 @@ class TvActivity : ComponentActivity() {
} else {
LoginScreen()
}
+
+ availableUpdate?.let { update ->
+ TvUpdateAvailableDialog(
+ update = update,
+ onAccept = updateViewModel::acceptUpdate,
+ onDecline = updateViewModel::declineUpdate
+ )
+ }
+ }
+
+ @Composable
+ private fun TvUpdateAvailableDialog(
+ update: AppUpdateInfo,
+ onAccept: () -> Unit,
+ onDecline: () -> Unit
+ ) {
+ val versionLabel = update.versionName?.takeIf { it.isNotBlank() } ?: update.versionCode.toString()
+ val releaseNotes = update.releaseNotes?.takeIf { it.isNotBlank() }
+ val updateText = if (releaseNotes == null) {
+ "Purefin TV $versionLabel is available.\n\nVersion code: ${update.versionCode}"
+ } else {
+ "Purefin TV $versionLabel is available.\n\nVersion code: ${update.versionCode}\n\n$releaseNotes"
+ }
+
+ AlertDialog(
+ onDismissRequest = onDecline,
+ title = { Text("Update available") },
+ text = { Text(updateText) },
+ confirmButton = {
+ TextButton(onClick = onAccept) {
+ Text("Install")
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = onDecline) {
+ Text("Not now")
+ }
+ }
+ )
}
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index cb74bc99..eb9534c2 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -20,6 +20,9 @@
android:supportsRtl="true"
android:usesCleartextTraffic="true"
android:theme="@style/Theme.Purefin">
+
diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/AppScreen.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/AppScreen.kt
index 38a36cc0..1e1b42e1 100644
--- a/app/src/main/java/hu/bbara/purefin/ui/screen/AppScreen.kt
+++ b/app/src/main/java/hu/bbara/purefin/ui/screen/AppScreen.kt
@@ -92,7 +92,7 @@ fun AppScreen(
)
},
onProfileClick = {},
- onCheckForUpdates = updateViewModel::checkForUpdates,
+ onCheckForUpdates = { updateViewModel.checkForUpdates() },
isCheckingForUpdates = isCheckingForUpdates,
onSettingsClick = viewModel::openSettings,
onLogoutClick = viewModel::logout,
diff --git a/app/src/main/java/hu/bbara/purefin/update/BuildConfigAppVersionProvider.kt b/app/src/main/java/hu/bbara/purefin/update/BuildConfigAppVersionProvider.kt
deleted file mode 100644
index 9452f625..00000000
--- a/app/src/main/java/hu/bbara/purefin/update/BuildConfigAppVersionProvider.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package hu.bbara.purefin.update
-
-import hu.bbara.purefin.BuildConfig
-import hu.bbara.purefin.feature.update.AppVersionProvider
-import javax.inject.Inject
-
-class BuildConfigAppVersionProvider @Inject constructor() : AppVersionProvider {
- override val versionCode: Long = BuildConfig.VERSION_CODE.toLong()
-}
diff --git a/core/src/main/java/hu/bbara/purefin/feature/update/AppUpdateRepository.kt b/core/src/main/java/hu/bbara/purefin/feature/update/AppUpdateRepository.kt
index d7b1e653..956c3645 100644
--- a/core/src/main/java/hu/bbara/purefin/feature/update/AppUpdateRepository.kt
+++ b/core/src/main/java/hu/bbara/purefin/feature/update/AppUpdateRepository.kt
@@ -15,7 +15,7 @@ class AppUpdateRepository @Inject constructor(
private val json = Json { ignoreUnknownKeys = true }
suspend fun checkForUpdate(): AppUpdateInfo? {
- val manifestUrl = "http://purefin.t.bbara.hu/app/update.json"
+ val manifestUrl = appVersionProvider.updateManifestUrl
if (manifestUrl.isBlank()) {
throw IllegalStateException("Update manifest URL not configured")
}
diff --git a/core/src/main/java/hu/bbara/purefin/feature/update/AppUpdateViewModel.kt b/core/src/main/java/hu/bbara/purefin/feature/update/AppUpdateViewModel.kt
index eaa4904e..eac78d9b 100644
--- a/core/src/main/java/hu/bbara/purefin/feature/update/AppUpdateViewModel.kt
+++ b/core/src/main/java/hu/bbara/purefin/feature/update/AppUpdateViewModel.kt
@@ -29,7 +29,7 @@ class AppUpdateViewModel @Inject constructor(
private var isInstallingUpdate = false
- fun checkForUpdates() {
+ fun checkForUpdates(showUpToDateMessage: Boolean = true) {
if (_isCheckingForUpdates.value) {
return
}
@@ -39,7 +39,9 @@ class AppUpdateViewModel @Inject constructor(
try {
val update = appUpdateRepository.checkForUpdate()
if (update == null) {
- _snackbarMessages.emit("Purefin is up to date")
+ if (showUpToDateMessage) {
+ _snackbarMessages.emit("Purefin is up to date")
+ }
} else {
_availableUpdate.value = update
}
diff --git a/core/src/main/java/hu/bbara/purefin/feature/update/AppVersionProvider.kt b/core/src/main/java/hu/bbara/purefin/feature/update/AppVersionProvider.kt
index bb2d7ab2..bc0264ae 100644
--- a/core/src/main/java/hu/bbara/purefin/feature/update/AppVersionProvider.kt
+++ b/core/src/main/java/hu/bbara/purefin/feature/update/AppVersionProvider.kt
@@ -2,4 +2,5 @@ package hu.bbara.purefin.feature.update
interface AppVersionProvider {
val versionCode: Long
+ val updateManifestUrl: String
}
diff --git a/core/src/main/java/hu/bbara/purefin/update/AndroidAppVersionProvider.kt b/core/src/main/java/hu/bbara/purefin/update/AndroidAppVersionProvider.kt
new file mode 100644
index 00000000..559156ad
--- /dev/null
+++ b/core/src/main/java/hu/bbara/purefin/update/AndroidAppVersionProvider.kt
@@ -0,0 +1,48 @@
+package hu.bbara.purefin.update
+
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageInfo
+import android.content.pm.PackageManager
+import android.os.Build
+import dagger.hilt.android.qualifiers.ApplicationContext
+import hu.bbara.purefin.feature.update.AppVersionProvider
+import javax.inject.Inject
+
+class AndroidAppVersionProvider @Inject constructor(
+ @ApplicationContext context: Context
+) : AppVersionProvider {
+ private val appContext = context.applicationContext
+
+ override val versionCode: Long
+ get() = appContext.packageManager.currentPackageInfo().longVersionCode
+
+ override val updateManifestUrl: String
+ get() = appContext.packageManager.currentApplicationInfo()
+ .metaData
+ ?.getString(UPDATE_MANIFEST_URL_META_DATA)
+ .orEmpty()
+
+ private fun PackageManager.currentPackageInfo(): PackageInfo =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ getPackageInfo(appContext.packageName, PackageManager.PackageInfoFlags.of(0))
+ } else {
+ @Suppress("DEPRECATION")
+ getPackageInfo(appContext.packageName, 0)
+ }
+
+ private fun PackageManager.currentApplicationInfo(): ApplicationInfo =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ getApplicationInfo(
+ appContext.packageName,
+ PackageManager.ApplicationInfoFlags.of(PackageManager.GET_META_DATA.toLong())
+ )
+ } else {
+ @Suppress("DEPRECATION")
+ getApplicationInfo(appContext.packageName, PackageManager.GET_META_DATA)
+ }
+
+ private companion object {
+ const val UPDATE_MANIFEST_URL_META_DATA = "hu.bbara.purefin.UPDATE_MANIFEST_URL"
+ }
+}
diff --git a/app/src/main/java/hu/bbara/purefin/update/AppUpdateInstallReceiver.kt b/core/src/main/java/hu/bbara/purefin/update/AppUpdateInstallReceiver.kt
similarity index 100%
rename from app/src/main/java/hu/bbara/purefin/update/AppUpdateInstallReceiver.kt
rename to core/src/main/java/hu/bbara/purefin/update/AppUpdateInstallReceiver.kt
diff --git a/app/src/main/java/hu/bbara/purefin/update/AppUpdateInstaller.kt b/core/src/main/java/hu/bbara/purefin/update/AppUpdateInstaller.kt
similarity index 86%
rename from app/src/main/java/hu/bbara/purefin/update/AppUpdateInstaller.kt
rename to core/src/main/java/hu/bbara/purefin/update/AppUpdateInstaller.kt
index 9a9aa857..8fd7cafc 100644
--- a/app/src/main/java/hu/bbara/purefin/update/AppUpdateInstaller.kt
+++ b/core/src/main/java/hu/bbara/purefin/update/AppUpdateInstaller.kt
@@ -35,7 +35,7 @@ class AndroidAppUpdateInstaller @Inject constructor(
val apkFile = withContext(Dispatchers.IO) {
downloadApk(update)
- .also { validateApk(it, update.versionCode) }
+ .also { validateApk(it) }
}
withContext(Dispatchers.IO) {
@@ -43,7 +43,7 @@ class AndroidAppUpdateInstaller @Inject constructor(
}
val versionLabel = update.versionName?.takeIf { it.isNotBlank() } ?: update.versionCode.toString()
- return "Downloaded Purefin $versionLabel"
+ return "Downloaded ${appLabel()} $versionLabel"
}
private fun downloadApk(update: AppUpdateInfo): File {
@@ -72,18 +72,9 @@ class AndroidAppUpdateInstaller @Inject constructor(
return apkFile
}
- private fun validateApk(apkFile: File, expectedVersionCode: Long) {
- val packageInfo = appContext.packageManager.getPackageArchiveInfo(apkFile.absolutePath, 0)
+ private fun validateApk(apkFile: File) {
+ appContext.packageManager.getPackageArchiveInfo(apkFile.absolutePath, 0)
?: throw IllegalStateException("Downloaded file is not a valid APK")
-// if (packageInfo.packageName != appContext.packageName) {
-// throw IllegalStateException("Downloaded APK package does not match Purefin")
-// }
-// if (packageInfo.longVersionCode != expectedVersionCode) {
-// throw IllegalStateException("Downloaded APK version does not match update manifest")
-// }
-// if (packageInfo.longVersionCode <= BuildConfig.VERSION_CODE.toLong()) {
-// throw IllegalStateException("Downloaded APK is not newer")
-// }
}
private fun commitInstallSession(apkFile: File, versionCode: Long) {
@@ -141,4 +132,7 @@ class AndroidAppUpdateInstaller @Inject constructor(
)
}
}
+
+ private fun appLabel(): String =
+ appContext.applicationInfo.loadLabel(appContext.packageManager).toString()
}
diff --git a/app/src/main/java/hu/bbara/purefin/update/AppUpdateModule.kt b/core/src/main/java/hu/bbara/purefin/update/AppUpdateModule.kt
similarity index 82%
rename from app/src/main/java/hu/bbara/purefin/update/AppUpdateModule.kt
rename to core/src/main/java/hu/bbara/purefin/update/AppUpdateModule.kt
index 28f6735b..c272a53d 100644
--- a/app/src/main/java/hu/bbara/purefin/update/AppUpdateModule.kt
+++ b/core/src/main/java/hu/bbara/purefin/update/AppUpdateModule.kt
@@ -15,5 +15,5 @@ abstract class AppUpdateModule {
abstract fun bindAppUpdateInstaller(impl: AndroidAppUpdateInstaller): AppUpdateInstaller
@Binds
- abstract fun bindAppVersionProvider(impl: BuildConfigAppVersionProvider): AppVersionProvider
+ abstract fun bindAppVersionProvider(impl: AndroidAppVersionProvider): AppVersionProvider
}