mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-11-06 02:40:58 +01:00
Fix crash on certain grid pages when the app is sent to the background (#1605)
## Description Fixes a crash when backgrounding/restoring the app on certain grid pages ### Related issues Fixes https://github.com/damontecres/Wholphin/issues/1598 Replaces #1599 ### Testing Emulator It's easy to reproduce by going to a library's recommended tab, scrolling to any row's view more, then pressing home to background the app ## Screenshots N/A ## AI or LLM usage None, but includes a commit from #1599 which did, see the note there --------- Co-authored-by: Christian Tismer <tismer@stackless.com>
This commit is contained in:
parent
8e0798ee16
commit
ae49e977ce
2 changed files with 251 additions and 217 deletions
|
|
@ -61,6 +61,7 @@ import com.github.damontecres.wholphin.ui.util.ProvideLocalClock
|
|||
import com.github.damontecres.wholphin.util.DebugLogTree
|
||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||
import com.github.damontecres.wholphin.util.WholphinDispatchers
|
||||
import com.github.damontecres.wholphin.util.requestSerializersModule
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
|
|
@ -141,6 +142,7 @@ class MainActivity : AppCompatActivity() {
|
|||
private val json =
|
||||
Json {
|
||||
classDiscriminator = "_type"
|
||||
serializersModule = requestSerializersModule
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
||||
|
|
@ -151,10 +153,22 @@ class MainActivity : AppCompatActivity() {
|
|||
lifecycle.addObserver(playbackLifecycleObserver)
|
||||
|
||||
val backStackStr = savedInstanceState?.getString(KEY_BACK_STACK)
|
||||
if (backStackStr != null) {
|
||||
val restoredBackStack =
|
||||
if (backStackStr != null) {
|
||||
try {
|
||||
json.decodeFromString<List<Destination>>(backStackStr)
|
||||
} catch (ex: Exception) {
|
||||
// Best-effort: a previously persisted back stack we can no longer decode
|
||||
// must not crash startup; fall back to a fresh start destination.
|
||||
Timber.w(ex, "Could not restore back stack; starting fresh")
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
if (restoredBackStack != null) {
|
||||
Timber.d("Restoring back stack")
|
||||
var backStack = json.decodeFromString<List<Destination>>(backStackStr)
|
||||
|
||||
var backStack = restoredBackStack
|
||||
if (!playExternalViewModel.launched.value) {
|
||||
val lastDest = backStack.lastOrNull()
|
||||
if (lastDest.isPlayback) {
|
||||
|
|
@ -320,8 +334,14 @@ 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)
|
||||
try {
|
||||
val str = json.encodeToString(navigationManager.backStack.toList())
|
||||
outState.putString(KEY_BACK_STACK, str)
|
||||
} catch (ex: Exception) {
|
||||
// Best-effort: some destinations carry types we can't serialize; skip persisting
|
||||
// the back stack rather than crashing in onSaveInstanceState.
|
||||
Timber.w(ex, "Could not persist back stack; skipping")
|
||||
}
|
||||
val playerBackend =
|
||||
runBlocking { userPreferencesDataStore.data.firstOrNull() }?.playbackPreferences?.playerBackend
|
||||
outState.putBoolean(KEY_EXTERNAL_PLAYER, playerBackend == PlayerBackend.EXTERNAL_PLAYER)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ import com.github.damontecres.wholphin.ui.DEFAULT_PAGE_SIZE
|
|||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import kotlinx.serialization.modules.polymorphic
|
||||
import kotlinx.serialization.modules.subclass
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.Response
|
||||
import org.jellyfin.sdk.api.client.extensions.genresApi
|
||||
|
|
@ -107,7 +110,8 @@ class ApiRequestPager<T>(
|
|||
/**
|
||||
* Specifies how a [RequestPager] should prepare and execute API calls
|
||||
*/
|
||||
interface RequestHandler<T> {
|
||||
@Serializable
|
||||
sealed interface RequestHandler<T> {
|
||||
/**
|
||||
* Prepare the given request with the specified parameters (eg which page to fetch)
|
||||
*/
|
||||
|
|
@ -128,245 +132,255 @@ interface RequestHandler<T> {
|
|||
}
|
||||
|
||||
@Serializable
|
||||
val GetItemsRequestHandler =
|
||||
object : RequestHandler<GetItemsRequest> {
|
||||
override fun prepare(
|
||||
request: GetItemsRequest,
|
||||
startIndex: Int,
|
||||
limit: Int,
|
||||
enableTotalRecordCount: Boolean,
|
||||
): GetItemsRequest =
|
||||
request.copy(
|
||||
startIndex = startIndex,
|
||||
limit = limit,
|
||||
enableTotalRecordCount = enableTotalRecordCount,
|
||||
)
|
||||
object GetItemsRequestHandler : RequestHandler<GetItemsRequest> {
|
||||
override fun prepare(
|
||||
request: GetItemsRequest,
|
||||
startIndex: Int,
|
||||
limit: Int,
|
||||
enableTotalRecordCount: Boolean,
|
||||
): GetItemsRequest =
|
||||
request.copy(
|
||||
startIndex = startIndex,
|
||||
limit = limit,
|
||||
enableTotalRecordCount = enableTotalRecordCount,
|
||||
)
|
||||
|
||||
override suspend fun execute(
|
||||
api: ApiClient,
|
||||
request: GetItemsRequest,
|
||||
): Response<BaseItemDtoQueryResult> = api.itemsApi.getItems(request)
|
||||
}
|
||||
override suspend fun execute(
|
||||
api: ApiClient,
|
||||
request: GetItemsRequest,
|
||||
): Response<BaseItemDtoQueryResult> = api.itemsApi.getItems(request)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
val GetEpisodesRequestHandler =
|
||||
object : RequestHandler<GetEpisodesRequest> {
|
||||
override fun prepare(
|
||||
request: GetEpisodesRequest,
|
||||
startIndex: Int,
|
||||
limit: Int,
|
||||
enableTotalRecordCount: Boolean,
|
||||
): GetEpisodesRequest =
|
||||
request.copy(
|
||||
startIndex = startIndex,
|
||||
limit = limit,
|
||||
)
|
||||
object GetEpisodesRequestHandler : RequestHandler<GetEpisodesRequest> {
|
||||
override fun prepare(
|
||||
request: GetEpisodesRequest,
|
||||
startIndex: Int,
|
||||
limit: Int,
|
||||
enableTotalRecordCount: Boolean,
|
||||
): GetEpisodesRequest =
|
||||
request.copy(
|
||||
startIndex = startIndex,
|
||||
limit = limit,
|
||||
)
|
||||
|
||||
override suspend fun execute(
|
||||
api: ApiClient,
|
||||
request: GetEpisodesRequest,
|
||||
): Response<BaseItemDtoQueryResult> = api.tvShowsApi.getEpisodes(request)
|
||||
}
|
||||
override suspend fun execute(
|
||||
api: ApiClient,
|
||||
request: GetEpisodesRequest,
|
||||
): Response<BaseItemDtoQueryResult> = api.tvShowsApi.getEpisodes(request)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
val GetResumeItemsRequestHandler =
|
||||
object : RequestHandler<GetResumeItemsRequest> {
|
||||
override fun prepare(
|
||||
request: GetResumeItemsRequest,
|
||||
startIndex: Int,
|
||||
limit: Int,
|
||||
enableTotalRecordCount: Boolean,
|
||||
): GetResumeItemsRequest =
|
||||
request.copy(
|
||||
startIndex = startIndex,
|
||||
limit = limit,
|
||||
enableTotalRecordCount = enableTotalRecordCount,
|
||||
)
|
||||
object GetResumeItemsRequestHandler : RequestHandler<GetResumeItemsRequest> {
|
||||
override fun prepare(
|
||||
request: GetResumeItemsRequest,
|
||||
startIndex: Int,
|
||||
limit: Int,
|
||||
enableTotalRecordCount: Boolean,
|
||||
): GetResumeItemsRequest =
|
||||
request.copy(
|
||||
startIndex = startIndex,
|
||||
limit = limit,
|
||||
enableTotalRecordCount = enableTotalRecordCount,
|
||||
)
|
||||
|
||||
override suspend fun execute(
|
||||
api: ApiClient,
|
||||
request: GetResumeItemsRequest,
|
||||
): Response<BaseItemDtoQueryResult> = api.itemsApi.getResumeItems(request)
|
||||
}
|
||||
override suspend fun execute(
|
||||
api: ApiClient,
|
||||
request: GetResumeItemsRequest,
|
||||
): Response<BaseItemDtoQueryResult> = api.itemsApi.getResumeItems(request)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
val GetNextUpRequestHandler =
|
||||
object : RequestHandler<GetNextUpRequest> {
|
||||
override fun prepare(
|
||||
request: GetNextUpRequest,
|
||||
startIndex: Int,
|
||||
limit: Int,
|
||||
enableTotalRecordCount: Boolean,
|
||||
): GetNextUpRequest =
|
||||
request.copy(
|
||||
startIndex = startIndex,
|
||||
limit = limit,
|
||||
enableTotalRecordCount = enableTotalRecordCount,
|
||||
)
|
||||
object GetNextUpRequestHandler : RequestHandler<GetNextUpRequest> {
|
||||
override fun prepare(
|
||||
request: GetNextUpRequest,
|
||||
startIndex: Int,
|
||||
limit: Int,
|
||||
enableTotalRecordCount: Boolean,
|
||||
): GetNextUpRequest =
|
||||
request.copy(
|
||||
startIndex = startIndex,
|
||||
limit = limit,
|
||||
enableTotalRecordCount = enableTotalRecordCount,
|
||||
)
|
||||
|
||||
override suspend fun execute(
|
||||
api: ApiClient,
|
||||
request: GetNextUpRequest,
|
||||
): Response<BaseItemDtoQueryResult> = api.tvShowsApi.getNextUp(request)
|
||||
}
|
||||
override suspend fun execute(
|
||||
api: ApiClient,
|
||||
request: GetNextUpRequest,
|
||||
): Response<BaseItemDtoQueryResult> = api.tvShowsApi.getNextUp(request)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
val GetSuggestionsRequestHandler =
|
||||
object : RequestHandler<GetSuggestionsRequest> {
|
||||
override fun prepare(
|
||||
request: GetSuggestionsRequest,
|
||||
startIndex: Int,
|
||||
limit: Int,
|
||||
enableTotalRecordCount: Boolean,
|
||||
): GetSuggestionsRequest =
|
||||
request.copy(
|
||||
startIndex = startIndex,
|
||||
limit = limit,
|
||||
enableTotalRecordCount = enableTotalRecordCount,
|
||||
)
|
||||
object GetSuggestionsRequestHandler : RequestHandler<GetSuggestionsRequest> {
|
||||
override fun prepare(
|
||||
request: GetSuggestionsRequest,
|
||||
startIndex: Int,
|
||||
limit: Int,
|
||||
enableTotalRecordCount: Boolean,
|
||||
): GetSuggestionsRequest =
|
||||
request.copy(
|
||||
startIndex = startIndex,
|
||||
limit = limit,
|
||||
enableTotalRecordCount = enableTotalRecordCount,
|
||||
)
|
||||
|
||||
override suspend fun execute(
|
||||
api: ApiClient,
|
||||
request: GetSuggestionsRequest,
|
||||
): Response<BaseItemDtoQueryResult> = api.suggestionsApi.getSuggestions(request)
|
||||
}
|
||||
override suspend fun execute(
|
||||
api: ApiClient,
|
||||
request: GetSuggestionsRequest,
|
||||
): Response<BaseItemDtoQueryResult> = api.suggestionsApi.getSuggestions(request)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
val GetPlaylistItemsRequestHandler =
|
||||
object : RequestHandler<GetPlaylistItemsRequest> {
|
||||
override fun prepare(
|
||||
request: GetPlaylistItemsRequest,
|
||||
startIndex: Int,
|
||||
limit: Int,
|
||||
enableTotalRecordCount: Boolean,
|
||||
): GetPlaylistItemsRequest =
|
||||
request.copy(
|
||||
startIndex = startIndex,
|
||||
limit = limit,
|
||||
)
|
||||
object GetPlaylistItemsRequestHandler : RequestHandler<GetPlaylistItemsRequest> {
|
||||
override fun prepare(
|
||||
request: GetPlaylistItemsRequest,
|
||||
startIndex: Int,
|
||||
limit: Int,
|
||||
enableTotalRecordCount: Boolean,
|
||||
): GetPlaylistItemsRequest =
|
||||
request.copy(
|
||||
startIndex = startIndex,
|
||||
limit = limit,
|
||||
)
|
||||
|
||||
override suspend fun execute(
|
||||
api: ApiClient,
|
||||
request: GetPlaylistItemsRequest,
|
||||
): Response<BaseItemDtoQueryResult> = api.playlistsApi.getPlaylistItems(request)
|
||||
}
|
||||
override suspend fun execute(
|
||||
api: ApiClient,
|
||||
request: GetPlaylistItemsRequest,
|
||||
): Response<BaseItemDtoQueryResult> = api.playlistsApi.getPlaylistItems(request)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
val GetGenresRequestHandler =
|
||||
object : RequestHandler<GetGenresRequest> {
|
||||
override fun prepare(
|
||||
request: GetGenresRequest,
|
||||
startIndex: Int,
|
||||
limit: Int,
|
||||
enableTotalRecordCount: Boolean,
|
||||
): GetGenresRequest =
|
||||
request.copy(
|
||||
startIndex = startIndex,
|
||||
limit = limit,
|
||||
enableTotalRecordCount = enableTotalRecordCount,
|
||||
)
|
||||
object GetGenresRequestHandler : RequestHandler<GetGenresRequest> {
|
||||
override fun prepare(
|
||||
request: GetGenresRequest,
|
||||
startIndex: Int,
|
||||
limit: Int,
|
||||
enableTotalRecordCount: Boolean,
|
||||
): GetGenresRequest =
|
||||
request.copy(
|
||||
startIndex = startIndex,
|
||||
limit = limit,
|
||||
enableTotalRecordCount = enableTotalRecordCount,
|
||||
)
|
||||
|
||||
override suspend fun execute(
|
||||
api: ApiClient,
|
||||
request: GetGenresRequest,
|
||||
): Response<BaseItemDtoQueryResult> = api.genresApi.getGenres(request)
|
||||
}
|
||||
|
||||
val GetProgramsDtoHandler =
|
||||
object : RequestHandler<GetProgramsDto> {
|
||||
override fun prepare(
|
||||
request: GetProgramsDto,
|
||||
startIndex: Int,
|
||||
limit: Int,
|
||||
enableTotalRecordCount: Boolean,
|
||||
): GetProgramsDto =
|
||||
request.copy(
|
||||
startIndex = startIndex,
|
||||
limit = limit,
|
||||
)
|
||||
|
||||
override suspend fun execute(
|
||||
api: ApiClient,
|
||||
request: GetProgramsDto,
|
||||
): Response<BaseItemDtoQueryResult> = api.liveTvApi.getPrograms(request)
|
||||
}
|
||||
override suspend fun execute(
|
||||
api: ApiClient,
|
||||
request: GetGenresRequest,
|
||||
): Response<BaseItemDtoQueryResult> = api.genresApi.getGenres(request)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
val GetPersonsHandler =
|
||||
object : RequestHandler<GetPersonsRequest> {
|
||||
override fun prepare(
|
||||
request: GetPersonsRequest,
|
||||
startIndex: Int,
|
||||
limit: Int,
|
||||
enableTotalRecordCount: Boolean,
|
||||
): GetPersonsRequest =
|
||||
request.copy(
|
||||
object GetProgramsDtoHandler : RequestHandler<GetProgramsDto> {
|
||||
override fun prepare(
|
||||
request: GetProgramsDto,
|
||||
startIndex: Int,
|
||||
limit: Int,
|
||||
enableTotalRecordCount: Boolean,
|
||||
): GetProgramsDto =
|
||||
request.copy(
|
||||
startIndex = startIndex,
|
||||
limit = limit,
|
||||
)
|
||||
|
||||
override suspend fun execute(
|
||||
api: ApiClient,
|
||||
request: GetProgramsDto,
|
||||
): Response<BaseItemDtoQueryResult> = api.liveTvApi.getPrograms(request)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
object GetPersonsHandler : RequestHandler<GetPersonsRequest> {
|
||||
override fun prepare(
|
||||
request: GetPersonsRequest,
|
||||
startIndex: Int,
|
||||
limit: Int,
|
||||
enableTotalRecordCount: Boolean,
|
||||
): GetPersonsRequest =
|
||||
request.copy(
|
||||
// startIndex = startIndex,
|
||||
limit = limit,
|
||||
)
|
||||
limit = limit,
|
||||
)
|
||||
|
||||
override suspend fun execute(
|
||||
api: ApiClient,
|
||||
request: GetPersonsRequest,
|
||||
): Response<BaseItemDtoQueryResult> = api.personsApi.getPersons((request))
|
||||
}
|
||||
override suspend fun execute(
|
||||
api: ApiClient,
|
||||
request: GetPersonsRequest,
|
||||
): Response<BaseItemDtoQueryResult> = api.personsApi.getPersons((request))
|
||||
}
|
||||
|
||||
val GetStudiosRequestHandler =
|
||||
object : RequestHandler<GetStudiosRequest> {
|
||||
override fun prepare(
|
||||
request: GetStudiosRequest,
|
||||
startIndex: Int,
|
||||
limit: Int,
|
||||
enableTotalRecordCount: Boolean,
|
||||
): GetStudiosRequest =
|
||||
request.copy(
|
||||
startIndex = startIndex,
|
||||
limit = limit,
|
||||
enableTotalRecordCount = enableTotalRecordCount,
|
||||
)
|
||||
@Serializable
|
||||
object GetStudiosRequestHandler : RequestHandler<GetStudiosRequest> {
|
||||
override fun prepare(
|
||||
request: GetStudiosRequest,
|
||||
startIndex: Int,
|
||||
limit: Int,
|
||||
enableTotalRecordCount: Boolean,
|
||||
): GetStudiosRequest =
|
||||
request.copy(
|
||||
startIndex = startIndex,
|
||||
limit = limit,
|
||||
enableTotalRecordCount = enableTotalRecordCount,
|
||||
)
|
||||
|
||||
override suspend fun execute(
|
||||
api: ApiClient,
|
||||
request: GetStudiosRequest,
|
||||
): Response<BaseItemDtoQueryResult> = api.studiosApi.getStudios(request)
|
||||
}
|
||||
override suspend fun execute(
|
||||
api: ApiClient,
|
||||
request: GetStudiosRequest,
|
||||
): Response<BaseItemDtoQueryResult> = api.studiosApi.getStudios(request)
|
||||
}
|
||||
|
||||
val GetRecordingsRequestHandler =
|
||||
object : RequestHandler<GetRecordingsRequest> {
|
||||
override fun prepare(
|
||||
request: GetRecordingsRequest,
|
||||
startIndex: Int,
|
||||
limit: Int,
|
||||
enableTotalRecordCount: Boolean,
|
||||
): GetRecordingsRequest =
|
||||
request.copy(
|
||||
startIndex = startIndex,
|
||||
limit = limit,
|
||||
enableTotalRecordCount = enableTotalRecordCount,
|
||||
)
|
||||
@Serializable
|
||||
object GetRecordingsRequestHandler : RequestHandler<GetRecordingsRequest> {
|
||||
override fun prepare(
|
||||
request: GetRecordingsRequest,
|
||||
startIndex: Int,
|
||||
limit: Int,
|
||||
enableTotalRecordCount: Boolean,
|
||||
): GetRecordingsRequest =
|
||||
request.copy(
|
||||
startIndex = startIndex,
|
||||
limit = limit,
|
||||
enableTotalRecordCount = enableTotalRecordCount,
|
||||
)
|
||||
|
||||
override suspend fun execute(
|
||||
api: ApiClient,
|
||||
request: GetRecordingsRequest,
|
||||
): Response<BaseItemDtoQueryResult> = api.liveTvApi.getRecordings(request)
|
||||
}
|
||||
override suspend fun execute(
|
||||
api: ApiClient,
|
||||
request: GetRecordingsRequest,
|
||||
): Response<BaseItemDtoQueryResult> = api.liveTvApi.getRecordings(request)
|
||||
}
|
||||
|
||||
val GetLiveTvChannelsRequestHandler =
|
||||
object : RequestHandler<GetLiveTvChannelsRequest> {
|
||||
override fun prepare(
|
||||
request: GetLiveTvChannelsRequest,
|
||||
startIndex: Int,
|
||||
limit: Int,
|
||||
enableTotalRecordCount: Boolean,
|
||||
): GetLiveTvChannelsRequest =
|
||||
request.copy(
|
||||
startIndex = startIndex,
|
||||
limit = limit,
|
||||
@Serializable
|
||||
object GetLiveTvChannelsRequestHandler : RequestHandler<GetLiveTvChannelsRequest> {
|
||||
override fun prepare(
|
||||
request: GetLiveTvChannelsRequest,
|
||||
startIndex: Int,
|
||||
limit: Int,
|
||||
enableTotalRecordCount: Boolean,
|
||||
): GetLiveTvChannelsRequest =
|
||||
request.copy(
|
||||
startIndex = startIndex,
|
||||
limit = limit,
|
||||
// enableTotalRecordCount = enableTotalRecordCount,
|
||||
)
|
||||
)
|
||||
|
||||
override suspend fun execute(
|
||||
api: ApiClient,
|
||||
request: GetLiveTvChannelsRequest,
|
||||
): Response<BaseItemDtoQueryResult> = api.liveTvApi.getLiveTvChannels(request)
|
||||
override suspend fun execute(
|
||||
api: ApiClient,
|
||||
request: GetLiveTvChannelsRequest,
|
||||
): Response<BaseItemDtoQueryResult> = api.liveTvApi.getLiveTvChannels(request)
|
||||
}
|
||||
|
||||
val requestSerializersModule =
|
||||
SerializersModule {
|
||||
polymorphic(Any::class) {
|
||||
subclass(GetItemsRequest::class)
|
||||
subclass(GetEpisodesRequest::class)
|
||||
subclass(GetResumeItemsRequest::class)
|
||||
subclass(GetNextUpRequest::class)
|
||||
subclass(GetSuggestionsRequest::class)
|
||||
subclass(GetPlaylistItemsRequest::class)
|
||||
subclass(GetGenresRequest::class)
|
||||
subclass(GetProgramsDto::class)
|
||||
subclass(GetPersonsRequest::class)
|
||||
subclass(GetStudiosRequest::class)
|
||||
subclass(GetRecordingsRequest::class)
|
||||
subclass(GetLiveTvChannelsRequest::class)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue