From 5fbe12e6cfbed6818dd945751d714224d55775a4 Mon Sep 17 00:00:00 2001 From: damontecres <154766448+damontecres@users.noreply.github.com> Date: Sun, 23 Nov 2025 17:19:01 -0500 Subject: [PATCH] Fix crash when go backing too quickly (#309) Fixes #306 Also fixes network-on-main and potential concurrency issues --- .../wholphin/services/DatePlayedService.kt | 48 +++++++++++-------- .../wholphin/services/NavigationManager.kt | 6 ++- .../wholphin/ui/main/HomeViewModel.kt | 42 ++++++++-------- 3 files changed, 54 insertions(+), 42 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/DatePlayedService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/DatePlayedService.kt index e2dcdbe1..16c12fca 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/DatePlayedService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/DatePlayedService.kt @@ -5,12 +5,17 @@ import androidx.appcompat.app.AppCompatActivity import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.preferences.AppPreference +import com.github.damontecres.wholphin.services.hilt.IoCoroutineScope import com.github.damontecres.wholphin.util.GetEpisodesRequestHandler import com.google.common.cache.CacheBuilder import com.google.common.cache.CacheLoader import dagger.hilt.android.qualifiers.ActivityContext import dagger.hilt.android.scopes.ActivityScoped -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.withContext import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.exception.InvalidStatusException import org.jellyfin.sdk.api.client.extensions.userLibraryApi @@ -28,50 +33,51 @@ class DatePlayedService @Inject constructor( private val api: ApiClient, + @param:IoCoroutineScope private val scope: CoroutineScope, ) { private val datePlayedCache = CacheBuilder .newBuilder() .maximumSize(AppPreference.HomePageItems.max) .expireAfterWrite(2, TimeUnit.HOURS) - .build( - object : - CacheLoader() { - override fun load(key: SeriesItemId): LocalDateTime = getLastPlayed(key) + .build>( + object : CacheLoader>() { + override fun load(key: SeriesItemId): Deferred = getLastPlayed(key) }, ) - private fun getLastPlayed(key: SeriesItemId): LocalDateTime { + private fun getLastPlayed(key: SeriesItemId): Deferred { val request = GetEpisodesRequest( seriesId = key.seriesId, adjacentTo = key.itemId, limit = 1, ) - return try { - val result = - runBlocking { + return scope.async(Dispatchers.IO) { + try { + val result = GetEpisodesRequestHandler .execute( api, request, ).content.items - } - result.firstOrNull()?.userData?.lastPlayedDate ?: LocalDateTime.MIN - } catch (ex: InvalidStatusException) { - Timber.w("Error fetching %s: %s", key, ex.localizedMessage) - LocalDateTime.MIN + result.firstOrNull()?.userData?.lastPlayedDate ?: LocalDateTime.MIN + } catch (ex: InvalidStatusException) { + Timber.w("Error fetching %s: %s", key, ex.localizedMessage) + LocalDateTime.MIN + } } } - suspend fun getLastPlayed(item: BaseItem): LocalDateTime? { - val seriesId = item.data.seriesId - return if (seriesId != null) { - datePlayedCache.get(SeriesItemId(seriesId, item.id)) - } else { - null + suspend fun getLastPlayed(item: BaseItem): LocalDateTime? = + withContext(Dispatchers.IO) { + val seriesId = item.data.seriesId + return@withContext if (seriesId != null) { + datePlayedCache.get(SeriesItemId(seriesId, item.id)).await() + } else { + null + } } - } fun invalidate(item: BaseItem) { item.data.seriesId?.let { seriesId -> diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/NavigationManager.kt b/app/src/main/java/com/github/damontecres/wholphin/services/NavigationManager.kt index d9e18ef5..a387ac8c 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/NavigationManager.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/NavigationManager.kt @@ -42,7 +42,11 @@ class NavigationManager * Go to the previous page */ fun goBack() { - backStack.removeLastOrNull() + synchronized(this) { + if (backStack.size > 1) { + backStack.removeLastOrNull() + } + } log() } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomeViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomeViewModel.kt index 36628755..bd2b0453 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomeViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomeViewModel.kt @@ -264,33 +264,35 @@ class HomeViewModel private suspend fun buildCombined( resume: List, nextUp: List, - ): List { - val start = System.currentTimeMillis() - val semaphore = Semaphore(3) - val deferred = - nextUp - .filter { it.data.seriesId != null } - .map { item -> - semaphore.withPermit { - viewModelScope.async { + ): List = + withContext(Dispatchers.IO) { + val start = System.currentTimeMillis() + val semaphore = Semaphore(3) + val deferred = + nextUp + .filter { it.data.seriesId != null } + .map { item -> + viewModelScope.async(Dispatchers.IO) { try { - datePlayedService.getLastPlayed(item) + semaphore.withPermit { + datePlayedService.getLastPlayed(item) + } } catch (ex: Exception) { Timber.e(ex, "Error fetching %s", item.id) null } } } - } - val nextUpLastPlayed = deferred.awaitAll() - val timestamps = mutableMapOf() - nextUp.map { it.id }.zip(nextUpLastPlayed).toMap(timestamps) - resume.forEach { timestamps[it.id] = it.data.userData?.lastPlayedDate } - val result = (resume + nextUp).sortedByDescending { timestamps[it.id] } - val duration = (System.currentTimeMillis() - start).milliseconds - Timber.v("buildCombined took %s", duration) - return result - } + + val nextUpLastPlayed = deferred.awaitAll() + val timestamps = mutableMapOf() + nextUp.map { it.id }.zip(nextUpLastPlayed).toMap(timestamps) + resume.forEach { timestamps[it.id] = it.data.userData?.lastPlayedDate } + val result = (resume + nextUp).sortedByDescending { timestamps[it.id] } + val duration = (System.currentTimeMillis() - start).milliseconds + Timber.v("buildCombined took %s", duration) + return@withContext result + } fun setWatched( itemId: UUID,