Fix issues when the system kills the app during playback (#1116)

## Description
This fixes some issues restoring the app state after it is killed by the
system, such as to free up memory while the screensaver is showing.

### Dev notes

Reproduce:
1. Start playing media from beginning
2. Pause at 10 minute mark
3. Wait for screensaver to start
4. Press a button
5. Media starts from beginning

Note: It may take a few iterations of 2-4 before the OS destroys the
app.

If the app is killed during playback, ideally Wholphin restores state to
_previous_ page, not playback since restoring playback is complex.
Before this PR, the back stack was maintained by Compose's
`rememberSerializable` and would be restored _after_ the
`PlaybackLifecycleObserver` cleans up the playback destination.

This meant that the app would be restored, from scratch, to the playback
page. The `PlaybackViewModel` would be initialized from scratch as it
was originally called, ie play from beginning.

The fix is to save and restore the back stack outside of Compose.

### Related issues
Fixes #767

### Testing
Emulator & nvidia shield

## Screenshots
N/A

## AI or LLM usage
None
This commit is contained in:
Ray 2026-03-18 18:09:13 -04:00 committed by GitHub
parent d184990b16
commit f9ff04c5b7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 39 additions and 30 deletions

View file

@ -24,6 +24,7 @@ import androidx.datastore.core.DataStore
import androidx.lifecycle.ViewModel
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewModelScope
import androidx.navigation3.runtime.NavBackStack
import androidx.tv.material3.ExperimentalTvMaterial3Api
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.preferences.AppPreference
@ -67,6 +68,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
@ -127,6 +129,11 @@ class MainActivity : AppCompatActivity() {
private var signInAuto = true
private val json =
Json {
classDiscriminator = "_type"
}
@OptIn(ExperimentalTvMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -134,6 +141,23 @@ class MainActivity : AppCompatActivity() {
Timber.i("MainActivity.onCreate: savedInstanceState is null=${savedInstanceState == null}")
lifecycle.addObserver(playbackLifecycleObserver)
val backStackStr = savedInstanceState?.getString(KEY_BACK_STACK)
if (backStackStr != null) {
Timber.d("Restoring back stack")
var backStack = json.decodeFromString<List<Destination>>(backStackStr)
val lastDest = backStack.lastOrNull()
if (lastDest is Destination.Playback ||
lastDest is Destination.PlaybackList ||
lastDest is Destination.Slideshow
) {
backStack = backStack.toMutableList().apply { removeAt(lastIndex) }
}
navigationManager.backStack = NavBackStack(*backStack.toTypedArray())
} else {
val startDestination = intent?.let(::extractDestination) ?: Destination.Home()
navigationManager.backStack = NavBackStack(startDestination)
}
viewModel.serverRepository.currentUser.observe(this) { user ->
if (user?.hasPin == true) {
window?.setFlags(
@ -206,17 +230,12 @@ class MainActivity : AppCompatActivity() {
appThemeColors = appPreferences.interfacePreferences.appThemeColors,
) {
ProvideLocalClock {
val requestedDestination =
remember(intent) {
intent?.let(::extractDestination) ?: Destination.Home()
}
MainContent(
backStack = setupNavigationManager.backStack,
navigationManager = navigationManager,
appPreferences = appPreferences,
backdropService = backdropService,
screensaverService = screensaverService,
requestedDestination = requestedDestination,
modifier = Modifier.fillMaxSize(),
)
}
@ -287,6 +306,8 @@ class MainActivity : AppCompatActivity() {
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
Timber.d("onSaveInstanceState")
val str = json.encodeToString(navigationManager.backStack.toList())
outState.putString(KEY_BACK_STACK, str)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
@ -362,6 +383,8 @@ class MainActivity : AppCompatActivity() {
const val INTENT_SEASON_NUMBER = "seaNum"
const val INTENT_SEASON_ID = "seaId"
private const val KEY_BACK_STACK = "backStack"
lateinit var instance: MainActivity
private set
}