mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
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:
parent
37d52ef57e
commit
9b995bf6ba
11 changed files with 147 additions and 42 deletions
|
|
@ -79,8 +79,12 @@ import dagger.hilt.android.AndroidEntryPoint
|
|||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||
|
|
@ -165,6 +169,20 @@ class MainActivity : AppCompatActivity() {
|
|||
window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
}
|
||||
}
|
||||
screensaverService.keepScreenOn
|
||||
.onEach { keepScreenOn ->
|
||||
Timber.v("keepScreenOn: %s", keepScreenOn)
|
||||
withContext(Dispatchers.Main) {
|
||||
if (keepScreenOn) {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
} else {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
}
|
||||
}
|
||||
}.catch { ex ->
|
||||
Timber.e(ex, "Error with keepScreenOn")
|
||||
}.launchIn(lifecycleScope)
|
||||
|
||||
viewModel.appStart()
|
||||
setContent {
|
||||
val appPreferences by userPreferencesDataStore.data.collectAsState(null)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package com.github.damontecres.wholphin
|
|||
import android.app.Application
|
||||
import android.os.Build
|
||||
import android.os.StrictMode
|
||||
import android.os.StrictMode.ThreadPolicy
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Composer
|
||||
import androidx.compose.runtime.ExperimentalComposeRuntimeApi
|
||||
|
|
@ -27,16 +26,6 @@ class WholphinApplication :
|
|||
init {
|
||||
instance = this
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
StrictMode.setThreadPolicy(
|
||||
ThreadPolicy
|
||||
.Builder()
|
||||
.detectNetwork()
|
||||
.penaltyLog()
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.plant(Timber.DebugTree())
|
||||
} else {
|
||||
|
|
@ -64,6 +53,23 @@ class WholphinApplication :
|
|||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
if (BuildConfig.DEBUG) {
|
||||
StrictMode.setThreadPolicy(
|
||||
StrictMode.ThreadPolicy
|
||||
.Builder()
|
||||
.detectNetwork()
|
||||
.penaltyLog()
|
||||
.penaltyDeathOnNetwork()
|
||||
.build(),
|
||||
)
|
||||
StrictMode.setVmPolicy(
|
||||
StrictMode.VmPolicy
|
||||
.Builder()
|
||||
.detectAll()
|
||||
.penaltyLog()
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
OkHttp.initialize(this)
|
||||
initAcra {
|
||||
buildConfigClass = BuildConfig::class.java
|
||||
|
|
|
|||
|
|
@ -42,8 +42,6 @@ class ServerRepository
|
|||
val apiClient: ApiClient,
|
||||
val userPreferencesDataStore: DataStore<AppPreferences>,
|
||||
) {
|
||||
private val sharedPreferences = getServerSharedPreferences(context)
|
||||
|
||||
private var _current = EqualityMutableLiveData<CurrentUser?>(null)
|
||||
val current: LiveData<CurrentUser?> = _current
|
||||
|
||||
|
|
@ -107,7 +105,7 @@ class ServerRepository
|
|||
_current.value = CurrentUser(updatedServer, updatedUser)
|
||||
_currentUserDto.value = userDto
|
||||
}
|
||||
sharedPreferences.edit(true) {
|
||||
getServerSharedPreferences(context).edit(true) {
|
||||
putString(SERVER_URL_KEY, updatedServer.url)
|
||||
putString(ACCESS_TOKEN_KEY, updatedUser.accessToken)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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? =
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ import org.jellyfin.sdk.api.client.ApiClient
|
|||
import org.jellyfin.sdk.api.client.Response
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.BaseItemDtoQueryResult
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
|
|
@ -429,12 +428,6 @@ fun Response<BaseItemDtoQueryResult>.toBaseItems(
|
|||
useSeriesForPrimary: Boolean,
|
||||
) = this.content.items.map { BaseItem.from(it, api, useSeriesForPrimary) }
|
||||
|
||||
@Composable
|
||||
fun rememberBackDropImage(item: BaseItem): String? {
|
||||
val imageUrlService = LocalImageUrlService.current
|
||||
return remember(item) { imageUrlService.getItemImageUrl(item, ImageType.BACKDROP) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this, coalescing nulls to zero, is greater than that
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -209,6 +209,7 @@ suspend fun getGenreImageMap(
|
|||
imageType = ImageType.BACKDROP,
|
||||
imageTags = item.imageTags.orEmpty(),
|
||||
fillWidth = cardWidthPx,
|
||||
backdropTags = item.backdropImageTags.orEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
package com.github.damontecres.wholphin.util
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.ApiClientFactory
|
||||
import org.jellyfin.sdk.api.client.HttpClientOptions
|
||||
import org.jellyfin.sdk.api.client.HttpMethod
|
||||
import org.jellyfin.sdk.api.client.RawResponse
|
||||
import org.jellyfin.sdk.api.okhttp.OkHttpFactory
|
||||
import org.jellyfin.sdk.api.sockets.SocketApi
|
||||
import org.jellyfin.sdk.api.sockets.SocketConnection
|
||||
import org.jellyfin.sdk.api.sockets.SocketConnectionFactory
|
||||
import org.jellyfin.sdk.model.ClientInfo
|
||||
import org.jellyfin.sdk.model.DeviceInfo
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
/**
|
||||
* Wraps [ApiClient.request] with the given [CoroutineContext]
|
||||
*/
|
||||
class CoroutineContextApiClient(
|
||||
private val client: ApiClient,
|
||||
private val coroutineContext: CoroutineContext = Dispatchers.IO,
|
||||
) : ApiClient() {
|
||||
override val baseUrl: String?
|
||||
get() = client.baseUrl
|
||||
override val accessToken: String?
|
||||
get() = client.accessToken
|
||||
override val clientInfo: ClientInfo
|
||||
get() = client.clientInfo
|
||||
override val deviceInfo: DeviceInfo
|
||||
get() = client.deviceInfo
|
||||
override val httpClientOptions: HttpClientOptions
|
||||
get() = client.httpClientOptions
|
||||
override val webSocket: SocketApi
|
||||
get() = client.webSocket
|
||||
|
||||
override fun update(
|
||||
baseUrl: String?,
|
||||
accessToken: String?,
|
||||
clientInfo: ClientInfo,
|
||||
deviceInfo: DeviceInfo,
|
||||
) {
|
||||
client.update(baseUrl, accessToken, clientInfo, deviceInfo)
|
||||
}
|
||||
|
||||
override suspend fun request(
|
||||
method: HttpMethod,
|
||||
pathTemplate: String,
|
||||
pathParameters: Map<String, Any?>,
|
||||
queryParameters: Map<String, Any?>,
|
||||
requestBody: Any?,
|
||||
): RawResponse =
|
||||
withContext(coroutineContext) {
|
||||
client.request(
|
||||
method,
|
||||
pathTemplate,
|
||||
pathParameters,
|
||||
queryParameters,
|
||||
requestBody,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class CoroutineContextApiClientFactory(
|
||||
private val factory: OkHttpFactory,
|
||||
private val coroutineContext: CoroutineContext = Dispatchers.IO,
|
||||
) : ApiClientFactory,
|
||||
SocketConnectionFactory {
|
||||
override fun create(
|
||||
baseUrl: String?,
|
||||
accessToken: String?,
|
||||
clientInfo: ClientInfo,
|
||||
deviceInfo: DeviceInfo,
|
||||
httpClientOptions: HttpClientOptions,
|
||||
socketConnectionFactory: SocketConnectionFactory,
|
||||
): ApiClient =
|
||||
CoroutineContextApiClient(
|
||||
factory.create(baseUrl, accessToken, clientInfo, deviceInfo, httpClientOptions, socketConnectionFactory),
|
||||
coroutineContext,
|
||||
)
|
||||
|
||||
override fun create(
|
||||
clientOptions: HttpClientOptions,
|
||||
scope: CoroutineScope,
|
||||
): SocketConnection = factory.create(clientOptions, scope)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue