From db89bdfdff5bb87923071e194b25f1650de8c924 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 7 Jun 2025 10:14:51 +0300 Subject: [PATCH] Respect network data saver #1390 --- .../koitharu/kotatsu/core/os/NetworkState.kt | 12 ++++++++ .../core/parser/MangaDataRepository.kt | 20 ++++++++++--- .../details/domain/DetailsLoadUseCase.kt | 30 ++++++++++++------- 3 files changed, 47 insertions(+), 15 deletions(-) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/os/NetworkState.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/os/NetworkState.kt index c05ec4593..8c70c8be0 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/os/NetworkState.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/os/NetworkState.kt @@ -2,6 +2,7 @@ package org.koitharu.kotatsu.core.os import android.net.ConnectivityManager import android.net.ConnectivityManager.NetworkCallback +import android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED import android.net.Network import android.net.NetworkCapabilities import android.net.NetworkRequest @@ -42,6 +43,17 @@ class NetworkState( connectivityManager.unregisterNetworkCallback(callback) } + fun isMetered(): Boolean { + return connectivityManager.isActiveNetworkMetered + } + + fun isDataSaverEnabled(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N + && connectivityManager.restrictBackgroundStatus == RESTRICT_BACKGROUND_STATUS_ENABLED + + fun isRestricted() = isMetered() && isDataSaverEnabled() + + fun isOfflineOrRestricted() = !isOnline() || isRestricted() + suspend fun awaitForConnection() { if (value) { return diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaDataRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaDataRepository.kt index 3aad4e2fb..ae11a85a6 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaDataRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaDataRepository.kt @@ -15,6 +15,7 @@ import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity import org.koitharu.kotatsu.core.db.entity.toEntities import org.koitharu.kotatsu.core.db.entity.toEntity import org.koitharu.kotatsu.core.db.entity.toManga +import org.koitharu.kotatsu.core.db.entity.toMangaChapters import org.koitharu.kotatsu.core.db.entity.toMangaTags import org.koitharu.kotatsu.core.model.LocalMangaSource import org.koitharu.kotatsu.core.model.isLocal @@ -119,10 +120,10 @@ class MangaDataRepository @Inject constructor( return db.getMangaDao().findByPublicUrl(publicUrl)?.toManga() } - suspend fun resolveIntent(intent: MangaIntent): Manga? = when { - intent.manga != null -> intent.manga - intent.mangaId != 0L -> findMangaById(intent.mangaId, true) - intent.uri != null -> resolverProvider.get().resolve(intent.uri) + suspend fun resolveIntent(intent: MangaIntent, withChapters: Boolean): Manga? = when { + intent.manga != null -> intent.manga.withCachedChaptersIfNeeded(withChapters) + intent.mangaId != 0L -> findMangaById(intent.mangaId, withChapters) + intent.uri != null -> resolverProvider.get().resolve(intent.uri).withCachedChaptersIfNeeded(withChapters) else -> null } @@ -184,6 +185,17 @@ class MangaDataRepository @Inject constructor( emitInitialState = emitInitialState, ) + private suspend fun Manga.withCachedChaptersIfNeeded(flag: Boolean): Manga = if (flag && chapters.isNullOrEmpty()) { + val cachedChapters = db.getChaptersDao().findAll(id) + if (cachedChapters.isEmpty()) { + this + } else { + copy(chapters = cachedChapters.toMangaChapters()) + } + } else { + this + } + private fun MangaPrefsEntity.getColorFilterOrNull(): ReaderColorFilter? { return if (cfBrightness != 0f || cfContrast != 0f || cfInvert || cfGrayscale) { ReaderColorFilter(cfBrightness, cfContrast, cfInvert, cfGrayscale) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DetailsLoadUseCase.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DetailsLoadUseCase.kt index b8b2a33f9..452c2f106 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DetailsLoadUseCase.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DetailsLoadUseCase.kt @@ -16,6 +16,7 @@ import kotlinx.coroutines.runInterruptible import okio.IOException import org.koitharu.kotatsu.core.model.isLocal import org.koitharu.kotatsu.core.nav.MangaIntent +import org.koitharu.kotatsu.core.os.NetworkState import org.koitharu.kotatsu.core.parser.CachingMangaRepository import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaRepository @@ -40,17 +41,12 @@ class DetailsLoadUseCase @Inject constructor( private val recoverUseCase: RecoverMangaUseCase, private val imageGetter: Html.ImageGetter, private val newChaptersUseCaseProvider: Provider, + private val networkState: NetworkState, ) { operator fun invoke(intent: MangaIntent, force: Boolean): Flow = channelFlow { - val manga = requireNotNull(mangaDataRepository.resolveIntent(intent)) { + val manga = requireNotNull(mangaDataRepository.resolveIntent(intent, withChapters = true)) { "Cannot resolve intent $intent" - }.let { m -> - if (m.chapters.isNullOrEmpty()) { - getCachedDetails(m.id) ?: m - } else { - m - } } val override = mangaDataRepository.getOverride(manga.id) send( @@ -69,6 +65,22 @@ class DetailsLoadUseCase @Inject constructor( } else { null } + if (!force && networkState.isOfflineOrRestricted()) { + // try to avoid loading if has saved manga + val localManga = local?.await() + if (manga.isLocal || localManga != null) { + send( + MangaDetails( + manga = manga, + localManga = localManga, + override = override, + description = manga.description?.parseAsHtml(withImages = true)?.trim(), + isLoaded = true, + ), + ) + return@channelFlow + } + } try { val details = getDetails(manga, force) launch { mangaDataRepository.updateChapters(details) } @@ -147,8 +159,4 @@ class DetailsLoadUseCase @Inject constructor( }.onFailure { e -> e.printStackTraceDebug() } - - private suspend fun getCachedDetails(mangaId: Long): Manga? = runCatchingCancellable { - mangaDataRepository.findMangaById(mangaId, withChapters = true) - }.getOrNull() }