Fix some IO related crashes (#1008)

## Description
- Catch network exceptions when querying to populate the nav drawer
- Perform screensaver work on IO thread
- Fix possible crash if screensaver service runs while regular activity
is destroyed
- Fix genre images & backdrop images not loading

This PR also adds an Jellyfin SDK `ApiClient` wrapper to push all
network calls onto the IO thread. And tries to use a more strict
network-on-main policy for debug builds.

### Related issues
Fixes #994
Fixes #1001
Should fix #1003

### Testing
Emulator with simulated exception throws

## Screenshots
N/A

## AI or LLM usage
None
This commit is contained in:
Ray 2026-02-26 15:48:39 -05:00 committed by GitHub
parent 37d52ef57e
commit 9b995bf6ba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 147 additions and 42 deletions

View file

@ -52,7 +52,7 @@ class BackdropService
if (item.type == BaseItemKind.GENRE) {
item.imageUrlOverride
} else {
imageUrlService.getItemImageUrl(item, ImageType.BACKDROP)!!
imageUrlService.getItemImageUrl(item, ImageType.BACKDROP)
}
submit(item.id.toString(), imageUrl)
}

View file

@ -28,11 +28,11 @@ class ImageUrlService
itemType: BaseItemKind,
seriesId: UUID?,
useSeriesForPrimary: Boolean,
imageTags: Map<ImageType, String?>,
imageType: ImageType,
imageTags: Map<ImageType, String?>,
backdropTags: List<String>,
parentThumbId: UUID? = null,
parentBackdropId: UUID? = null,
backdropTags: List<String> = emptyList(),
fillWidth: Int? = null,
fillHeight: Int? = null,
): String? =

View file

@ -11,11 +11,13 @@ import com.github.damontecres.wholphin.ui.main.settings.Library
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.nav.NavDrawerItem
import com.github.damontecres.wholphin.ui.nav.ServerNavDrawerItem
import com.github.damontecres.wholphin.ui.showToast
import com.github.damontecres.wholphin.util.supportedCollectionTypes
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@ -60,7 +62,11 @@ class NavDrawerService
if (user != null && userDto != null && user.id == userDto.id) {
updateNavDrawer(user, userDto)
}
}.catch { ex ->
Timber.e(ex, "Error updating nav drawer")
showToast(context, "Error fetching user's views")
}.launchIn(coroutineScope)
seerrServerRepository.active
.onEach { discoverActive ->
_state.update { it.copy(discoverEnabled = discoverActive) }

View file

@ -1,13 +1,12 @@
package com.github.damontecres.wholphin.services
import android.content.Context
import android.view.WindowManager
import coil3.imageLoader
import coil3.request.ImageRequest
import com.github.damontecres.wholphin.MainActivity
import com.github.damontecres.wholphin.services.hilt.DefaultCoroutineScope
import com.github.damontecres.wholphin.ui.components.CurrentItem
import com.github.damontecres.wholphin.ui.formatDate
import com.github.damontecres.wholphin.ui.launchDefault
import com.github.damontecres.wholphin.util.ApiRequestPager
import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
@ -23,11 +22,11 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.cancellable
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.libraryApi
import org.jellyfin.sdk.model.api.BaseItemKind
@ -52,6 +51,8 @@ class ScreensaverService
private val _state = MutableStateFlow(ScreensaverState(false, false, false, false))
val state: StateFlow<ScreensaverState> = _state
val keepScreenOn = MutableStateFlow(false)
private var waitJob: Job? = null
init {
@ -114,7 +115,7 @@ class ScreensaverService
}
fun keepScreenOn(keep: Boolean) {
scope.launch {
scope.launchDefault {
val screensaverEnabled = _state.value.enabled
Timber.d("Keep screen on: %s, screensaverEnabled=%s", keep, screensaverEnabled)
if (screensaverEnabled) {
@ -131,15 +132,9 @@ class ScreensaverService
}
}
private suspend fun keepScreenOnInternal(keep: Boolean) =
withContext(Dispatchers.Main) {
val window = MainActivity.instance.window
if (keep) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} else {
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
}
private fun keepScreenOnInternal(keep: Boolean) {
keepScreenOn.update { keep }
}
fun createItemFlow(scope: CoroutineScope): Flow<CurrentItem?> =
flow {
@ -186,7 +181,7 @@ class ScreensaverService
}
val logoUrl = imageUrlService.getItemImageUrl(item, ImageType.LOGO)
if (backdropUrl != null) {
val result =
try {
context.imageLoader
.enqueue(
ImageRequest
@ -195,7 +190,6 @@ class ScreensaverService
.build(),
).job
.await()
try {
emit(CurrentItem(item, backdropUrl, logoUrl, title ?: ""))
} catch (_: CancellationException) {
break
@ -211,7 +205,7 @@ class ScreensaverService
index++
}
}
}.cancellable()
}.flowOn(Dispatchers.Default).cancellable()
}
data class ScreensaverState(

View file

@ -10,6 +10,7 @@ import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.preferences.updateInterfacePreferences
import com.github.damontecres.wholphin.services.SeerrApi
import com.github.damontecres.wholphin.util.CoroutineContextApiClientFactory
import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.RememberTabManager
import dagger.Module
@ -111,12 +112,12 @@ object AppModule {
@Singleton
fun okHttpFactory(
@StandardOkHttpClient okHttpClient: OkHttpClient,
) = OkHttpFactory(okHttpClient)
) = CoroutineContextApiClientFactory(OkHttpFactory(okHttpClient))
@Provides
@Singleton
fun jellyfin(
okHttpFactory: OkHttpFactory,
okHttpFactory: CoroutineContextApiClientFactory,
@ApplicationContext context: Context,
clientInfo: ClientInfo,
deviceInfo: DeviceInfo,