feat(tv-update): implement update check and dialog for available updates on android tv devices

This commit is contained in:
2026-05-03 14:49:19 +02:00
parent a5bb1f84dc
commit fe0b76a0ad
12 changed files with 139 additions and 34 deletions

View File

@@ -1,10 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-feature
android:name="android.hardware.touchscreen"
@@ -23,7 +25,11 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/purefin_logo_round"
android:supportsRtl="true"
android:usesCleartextTraffic="true"
android:theme="@style/Theme.Purefin">
<meta-data
android:name="hu.bbara.purefin.UPDATE_MANIFEST_URL"
android:value="http://purefin.t.bbara.hu/app-tv/update.json" />
<activity
android:name="hu.bbara.purefin.tv.TvActivity"
android:exported="true"
@@ -34,6 +40,9 @@
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
<receiver
android:name="hu.bbara.purefin.update.AppUpdateInstallReceiver"
android:exported="false" />
</application>
</manifest>

View File

@@ -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<Route>.() -> 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<Route>
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")
}
}
)
}
}