diff --git a/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt b/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt index 6aa749f1..8d9beccb 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt @@ -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>(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>(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) diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/ApiRequestPager.kt b/app/src/main/java/com/github/damontecres/wholphin/util/ApiRequestPager.kt index 32b66d04..555a5a51 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/ApiRequestPager.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/ApiRequestPager.kt @@ -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( /** * Specifies how a [RequestPager] should prepare and execute API calls */ -interface RequestHandler { +@Serializable +sealed interface RequestHandler { /** * Prepare the given request with the specified parameters (eg which page to fetch) */ @@ -128,245 +132,255 @@ interface RequestHandler { } @Serializable -val GetItemsRequestHandler = - object : RequestHandler { - override fun prepare( - request: GetItemsRequest, - startIndex: Int, - limit: Int, - enableTotalRecordCount: Boolean, - ): GetItemsRequest = - request.copy( - startIndex = startIndex, - limit = limit, - enableTotalRecordCount = enableTotalRecordCount, - ) +object GetItemsRequestHandler : RequestHandler { + 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 = api.itemsApi.getItems(request) - } + override suspend fun execute( + api: ApiClient, + request: GetItemsRequest, + ): Response = api.itemsApi.getItems(request) +} @Serializable -val GetEpisodesRequestHandler = - object : RequestHandler { - override fun prepare( - request: GetEpisodesRequest, - startIndex: Int, - limit: Int, - enableTotalRecordCount: Boolean, - ): GetEpisodesRequest = - request.copy( - startIndex = startIndex, - limit = limit, - ) +object GetEpisodesRequestHandler : RequestHandler { + 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 = api.tvShowsApi.getEpisodes(request) - } + override suspend fun execute( + api: ApiClient, + request: GetEpisodesRequest, + ): Response = api.tvShowsApi.getEpisodes(request) +} @Serializable -val GetResumeItemsRequestHandler = - object : RequestHandler { - override fun prepare( - request: GetResumeItemsRequest, - startIndex: Int, - limit: Int, - enableTotalRecordCount: Boolean, - ): GetResumeItemsRequest = - request.copy( - startIndex = startIndex, - limit = limit, - enableTotalRecordCount = enableTotalRecordCount, - ) +object GetResumeItemsRequestHandler : RequestHandler { + 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 = api.itemsApi.getResumeItems(request) - } + override suspend fun execute( + api: ApiClient, + request: GetResumeItemsRequest, + ): Response = api.itemsApi.getResumeItems(request) +} @Serializable -val GetNextUpRequestHandler = - object : RequestHandler { - override fun prepare( - request: GetNextUpRequest, - startIndex: Int, - limit: Int, - enableTotalRecordCount: Boolean, - ): GetNextUpRequest = - request.copy( - startIndex = startIndex, - limit = limit, - enableTotalRecordCount = enableTotalRecordCount, - ) +object GetNextUpRequestHandler : RequestHandler { + 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 = api.tvShowsApi.getNextUp(request) - } + override suspend fun execute( + api: ApiClient, + request: GetNextUpRequest, + ): Response = api.tvShowsApi.getNextUp(request) +} @Serializable -val GetSuggestionsRequestHandler = - object : RequestHandler { - override fun prepare( - request: GetSuggestionsRequest, - startIndex: Int, - limit: Int, - enableTotalRecordCount: Boolean, - ): GetSuggestionsRequest = - request.copy( - startIndex = startIndex, - limit = limit, - enableTotalRecordCount = enableTotalRecordCount, - ) +object GetSuggestionsRequestHandler : RequestHandler { + 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 = api.suggestionsApi.getSuggestions(request) - } + override suspend fun execute( + api: ApiClient, + request: GetSuggestionsRequest, + ): Response = api.suggestionsApi.getSuggestions(request) +} @Serializable -val GetPlaylistItemsRequestHandler = - object : RequestHandler { - override fun prepare( - request: GetPlaylistItemsRequest, - startIndex: Int, - limit: Int, - enableTotalRecordCount: Boolean, - ): GetPlaylistItemsRequest = - request.copy( - startIndex = startIndex, - limit = limit, - ) +object GetPlaylistItemsRequestHandler : RequestHandler { + 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 = api.playlistsApi.getPlaylistItems(request) - } + override suspend fun execute( + api: ApiClient, + request: GetPlaylistItemsRequest, + ): Response = api.playlistsApi.getPlaylistItems(request) +} @Serializable -val GetGenresRequestHandler = - object : RequestHandler { - override fun prepare( - request: GetGenresRequest, - startIndex: Int, - limit: Int, - enableTotalRecordCount: Boolean, - ): GetGenresRequest = - request.copy( - startIndex = startIndex, - limit = limit, - enableTotalRecordCount = enableTotalRecordCount, - ) +object GetGenresRequestHandler : RequestHandler { + 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 = api.genresApi.getGenres(request) - } - -val GetProgramsDtoHandler = - object : RequestHandler { - 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 = api.liveTvApi.getPrograms(request) - } + override suspend fun execute( + api: ApiClient, + request: GetGenresRequest, + ): Response = api.genresApi.getGenres(request) +} @Serializable -val GetPersonsHandler = - object : RequestHandler { - override fun prepare( - request: GetPersonsRequest, - startIndex: Int, - limit: Int, - enableTotalRecordCount: Boolean, - ): GetPersonsRequest = - request.copy( +object GetProgramsDtoHandler : RequestHandler { + 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 = api.liveTvApi.getPrograms(request) +} + +@Serializable +object GetPersonsHandler : RequestHandler { + 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 = api.personsApi.getPersons((request)) - } + override suspend fun execute( + api: ApiClient, + request: GetPersonsRequest, + ): Response = api.personsApi.getPersons((request)) +} -val GetStudiosRequestHandler = - object : RequestHandler { - override fun prepare( - request: GetStudiosRequest, - startIndex: Int, - limit: Int, - enableTotalRecordCount: Boolean, - ): GetStudiosRequest = - request.copy( - startIndex = startIndex, - limit = limit, - enableTotalRecordCount = enableTotalRecordCount, - ) +@Serializable +object GetStudiosRequestHandler : RequestHandler { + 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 = api.studiosApi.getStudios(request) - } + override suspend fun execute( + api: ApiClient, + request: GetStudiosRequest, + ): Response = api.studiosApi.getStudios(request) +} -val GetRecordingsRequestHandler = - object : RequestHandler { - override fun prepare( - request: GetRecordingsRequest, - startIndex: Int, - limit: Int, - enableTotalRecordCount: Boolean, - ): GetRecordingsRequest = - request.copy( - startIndex = startIndex, - limit = limit, - enableTotalRecordCount = enableTotalRecordCount, - ) +@Serializable +object GetRecordingsRequestHandler : RequestHandler { + 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 = api.liveTvApi.getRecordings(request) - } + override suspend fun execute( + api: ApiClient, + request: GetRecordingsRequest, + ): Response = api.liveTvApi.getRecordings(request) +} -val GetLiveTvChannelsRequestHandler = - object : RequestHandler { - override fun prepare( - request: GetLiveTvChannelsRequest, - startIndex: Int, - limit: Int, - enableTotalRecordCount: Boolean, - ): GetLiveTvChannelsRequest = - request.copy( - startIndex = startIndex, - limit = limit, +@Serializable +object GetLiveTvChannelsRequestHandler : RequestHandler { + 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 = api.liveTvApi.getLiveTvChannels(request) + override suspend fun execute( + api: ApiClient, + request: GetLiveTvChannelsRequest, + ): Response = 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) + } }