From e6ae9e8bd6d6f5eddf2cbfc5f635a844a96f9377 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 4 Jan 2023 07:40:42 +0200 Subject: [PATCH] Prefetch chapter --- app/src/main/AndroidManifest.xml | 5 +- .../org/koitharu/kotatsu/core/AppModule.kt | 16 ++++ .../kotatsu/core/cache/ContentCache.kt | 22 +++++ .../kotatsu/core/cache/DeferredLruCache.kt | 17 ++++ .../kotatsu/core/cache/MemoryContentCache.kt | 29 ++++++ .../kotatsu/core/cache/StubContentCache.kt | 17 ++++ .../kotatsu/core/parser/MangaRepository.kt | 17 +++- .../core/parser/RemoteMangaRepository.kt | 36 ++++++- .../details/service/MangaPrefetchService.kt | 96 +++++++++++++++++++ .../service/PrefetchCompanionEntryPoint.kt | 14 +++ .../kotatsu/details/ui/DetailsActivity.kt | 22 +++++ 11 files changed, 280 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/cache/ContentCache.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/cache/DeferredLruCache.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/cache/MemoryContentCache.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/cache/StubContentCache.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/details/service/MangaPrefetchService.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/details/service/PrefetchCompanionEntryPoint.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index be81140ca..c3f2bd0ab 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -188,6 +188,9 @@ android:name="android.content.SyncAdapter" android:resource="@xml/sync_history" /> + + android:value="@bool/com_samsung_android_icon_container_has_icon_container" /> diff --git a/app/src/main/java/org/koitharu/kotatsu/core/AppModule.kt b/app/src/main/java/org/koitharu/kotatsu/core/AppModule.kt index 576e5f5a6..8a39c05a1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/AppModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/AppModule.kt @@ -24,6 +24,9 @@ import okhttp3.CookieJar import okhttp3.OkHttpClient import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.base.ui.util.ActivityRecreationHandle +import org.koitharu.kotatsu.core.cache.ContentCache +import org.koitharu.kotatsu.core.cache.MemoryContentCache +import org.koitharu.kotatsu.core.cache.StubContentCache import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.network.* import org.koitharu.kotatsu.core.network.cookies.AndroidCookieJar @@ -44,6 +47,7 @@ import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider import org.koitharu.kotatsu.settings.backup.BackupObserver import org.koitharu.kotatsu.sync.domain.SyncController import org.koitharu.kotatsu.utils.IncognitoModeIndicator +import org.koitharu.kotatsu.utils.ext.activityManager import org.koitharu.kotatsu.utils.ext.connectivityManager import org.koitharu.kotatsu.utils.ext.isLowRamDevice import org.koitharu.kotatsu.utils.image.CoilImageGetter @@ -182,5 +186,17 @@ interface AppModule { activityRecreationHandle, incognitoModeIndicator, ) + + @Provides + @Singleton + fun provideContentCache( + @ApplicationContext context: Context, + ): ContentCache { + return if (context.activityManager?.isLowRamDevice == true) { + StubContentCache() + } else { + MemoryContentCache() + } + } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/core/cache/ContentCache.kt b/app/src/main/java/org/koitharu/kotatsu/core/cache/ContentCache.kt new file mode 100644 index 000000000..56a6f740e --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/cache/ContentCache.kt @@ -0,0 +1,22 @@ +package org.koitharu.kotatsu.core.cache + +import kotlinx.coroutines.Deferred +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaPage +import org.koitharu.kotatsu.parsers.model.MangaSource + +interface ContentCache { + + suspend fun getDetails(source: MangaSource, url: String): Manga? + + fun putDetails(source: MangaSource, url: String, details: Deferred) + + suspend fun getPages(source: MangaSource, url: String): List? + + fun putPages(source: MangaSource, url: String, pages: Deferred>) + + data class Key( + val source: MangaSource, + val url: String, + ) +} diff --git a/app/src/main/java/org/koitharu/kotatsu/core/cache/DeferredLruCache.kt b/app/src/main/java/org/koitharu/kotatsu/core/cache/DeferredLruCache.kt new file mode 100644 index 000000000..7202543fd --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/cache/DeferredLruCache.kt @@ -0,0 +1,17 @@ +package org.koitharu.kotatsu.core.cache + +import androidx.collection.LruCache +import kotlinx.coroutines.Deferred + +class DeferredLruCache(maxSize: Int) : LruCache>(maxSize) { + + override fun entryRemoved( + evicted: Boolean, + key: ContentCache.Key, + oldValue: Deferred, + newValue: Deferred?, + ) { + super.entryRemoved(evicted, key, oldValue, newValue) + oldValue.cancel() + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/core/cache/MemoryContentCache.kt b/app/src/main/java/org/koitharu/kotatsu/core/cache/MemoryContentCache.kt new file mode 100644 index 000000000..ed1b46ac1 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/cache/MemoryContentCache.kt @@ -0,0 +1,29 @@ +package org.koitharu.kotatsu.core.cache + +import kotlinx.coroutines.Deferred +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaPage +import org.koitharu.kotatsu.parsers.model.MangaSource + +@Suppress("DeferredResultUnused") +class MemoryContentCache : ContentCache { + + private val detailsCache = DeferredLruCache(10) + private val pagesCache = DeferredLruCache>(10) + + override suspend fun getDetails(source: MangaSource, url: String): Manga? { + return detailsCache[ContentCache.Key(source, url)]?.await() + } + + override fun putDetails(source: MangaSource, url: String, details: Deferred) { + detailsCache.put(ContentCache.Key(source, url), details) + } + + override suspend fun getPages(source: MangaSource, url: String): List? { + return pagesCache[ContentCache.Key(source, url)]?.await() + } + + override fun putPages(source: MangaSource, url: String, pages: Deferred>) { + pagesCache.put(ContentCache.Key(source, url), pages) + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/core/cache/StubContentCache.kt b/app/src/main/java/org/koitharu/kotatsu/core/cache/StubContentCache.kt new file mode 100644 index 000000000..cd3d632be --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/cache/StubContentCache.kt @@ -0,0 +1,17 @@ +package org.koitharu.kotatsu.core.cache + +import kotlinx.coroutines.Deferred +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaPage +import org.koitharu.kotatsu.parsers.model.MangaSource + +class StubContentCache : ContentCache { + + override suspend fun getDetails(source: MangaSource, url: String): Manga? = null + + override fun putDetails(source: MangaSource, url: String, details: Deferred) = Unit + + override suspend fun getPages(source: MangaSource, url: String): List? = null + + override fun putPages(source: MangaSource, url: String, pages: Deferred>) = Unit +} diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepository.kt index 5c1c71e82..934acc008 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepository.kt @@ -1,13 +1,19 @@ package org.koitharu.kotatsu.core.parser +import org.koitharu.kotatsu.core.cache.ContentCache +import org.koitharu.kotatsu.local.domain.LocalMangaRepository +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaChapter +import org.koitharu.kotatsu.parsers.model.MangaPage +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.model.MangaTag +import org.koitharu.kotatsu.parsers.model.SortOrder import java.lang.ref.WeakReference -import java.util.* +import java.util.EnumMap import javax.inject.Inject import javax.inject.Singleton import kotlin.collections.set -import org.koitharu.kotatsu.local.domain.LocalMangaRepository -import org.koitharu.kotatsu.parsers.MangaLoaderContext -import org.koitharu.kotatsu.parsers.model.* interface MangaRepository { @@ -31,6 +37,7 @@ interface MangaRepository { class Factory @Inject constructor( private val localMangaRepository: LocalMangaRepository, private val loaderContext: MangaLoaderContext, + private val contentCache: ContentCache, ) { private val cache = EnumMap>(MangaSource::class.java) @@ -42,7 +49,7 @@ interface MangaRepository { cache[source]?.get()?.let { return it } return synchronized(cache) { cache[source]?.get()?.let { return it } - val repository = RemoteMangaRepository(MangaParser(source, loaderContext)) + val repository = RemoteMangaRepository(MangaParser(source, loaderContext), contentCache) cache[source] = WeakReference(repository) repository } diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt index f98634436..be7b3b6a5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt @@ -1,12 +1,24 @@ package org.koitharu.kotatsu.core.parser +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import org.koitharu.kotatsu.core.cache.ContentCache import org.koitharu.kotatsu.core.prefs.SourceSettings import org.koitharu.kotatsu.parsers.MangaParser import org.koitharu.kotatsu.parsers.MangaParserAuthProvider import org.koitharu.kotatsu.parsers.config.ConfigKey -import org.koitharu.kotatsu.parsers.model.* +import org.koitharu.kotatsu.parsers.model.Favicons +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaChapter +import org.koitharu.kotatsu.parsers.model.MangaPage +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.model.MangaTag +import org.koitharu.kotatsu.parsers.model.SortOrder -class RemoteMangaRepository(private val parser: MangaParser) : MangaRepository { +class RemoteMangaRepository( + private val parser: MangaParser, + private val cache: ContentCache, +) : MangaRepository { override val source: MangaSource get() = parser.source @@ -28,9 +40,23 @@ class RemoteMangaRepository(private val parser: MangaParser) : MangaRepository { return parser.getList(offset, tags, sortOrder) } - override suspend fun getDetails(manga: Manga): Manga = parser.getDetails(manga) + override suspend fun getDetails(manga: Manga): Manga { + cache.getDetails(source, manga.url)?.let { return it } + return coroutineScope { + val details = async { parser.getDetails(manga) } + cache.putDetails(source, manga.url, details) + details + }.await() + } - override suspend fun getPages(chapter: MangaChapter): List = parser.getPages(chapter) + override suspend fun getPages(chapter: MangaChapter): List { + cache.getPages(source, chapter.url)?.let { return it } + return coroutineScope { + val pages = async { parser.getPages(chapter) } + cache.putPages(source, chapter.url, pages) + pages + }.await() + } override suspend fun getPageUrl(page: MangaPage): String = parser.getPageUrl(page) @@ -45,4 +71,4 @@ class RemoteMangaRepository(private val parser: MangaParser) : MangaRepository { } private fun getConfig() = parser.config as SourceSettings -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/details/service/MangaPrefetchService.kt b/app/src/main/java/org/koitharu/kotatsu/details/service/MangaPrefetchService.kt new file mode 100644 index 000000000..a579c5d96 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/details/service/MangaPrefetchService.kt @@ -0,0 +1,96 @@ +package org.koitharu.kotatsu.details.service + +import android.content.Context +import android.content.Intent +import dagger.hilt.android.AndroidEntryPoint +import dagger.hilt.android.EntryPointAccessors +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import org.koitharu.kotatsu.base.ui.CoroutineIntentService +import org.koitharu.kotatsu.core.cache.ContentCache +import org.koitharu.kotatsu.core.cache.StubContentCache +import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga +import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaChapters +import org.koitharu.kotatsu.core.parser.MangaRepository +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaChapter +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat +import org.koitharu.kotatsu.utils.ext.processLifecycleScope +import org.koitharu.kotatsu.utils.ext.runCatchingCancellable +import javax.inject.Inject + +@AndroidEntryPoint +class MangaPrefetchService : CoroutineIntentService() { + + @Inject + lateinit var mangaRepositoryFactory: MangaRepository.Factory + + @Inject + lateinit var cache: ContentCache + + override suspend fun processIntent(startId: Int, intent: Intent) { + when (intent.action) { + ACTION_PREFETCH_DETAILS -> prefetchDetails( + manga = intent.getParcelableExtraCompat(EXTRA_MANGA)?.manga ?: return, + ) + + ACTION_PREFETCH_PAGES -> prefetchPages( + chapter = intent.getParcelableExtraCompat(EXTRA_CHAPTER) + ?.chapters?.singleOrNull() ?: return, + ) + } + } + + override fun onError(startId: Int, error: Throwable) = Unit + + private suspend fun prefetchDetails(manga: Manga) = coroutineScope { + val source = mangaRepositoryFactory.create(manga.source) + processLifecycleScope.launch(Dispatchers.Default) { + runCatchingCancellable { source.getDetails(manga) } + }.join() + } + + private suspend fun prefetchPages(chapter: MangaChapter) { + val source = mangaRepositoryFactory.create(chapter.source) + processLifecycleScope.launch(Dispatchers.Default) { + runCatchingCancellable { source.getPages(chapter) } + }.join() + } + + companion object { + + private const val EXTRA_MANGA = "manga" + private const val EXTRA_CHAPTER = "manga" + private const val ACTION_PREFETCH_DETAILS = "details" + private const val ACTION_PREFETCH_PAGES = "pages" + + fun prefetchDetails(context: Context, manga: Manga) { + if (!isPrefetchAvailable(context, manga.source)) return + val intent = Intent(context, MangaPrefetchService::class.java) + intent.action = ACTION_PREFETCH_DETAILS + intent.putExtra(EXTRA_MANGA, ParcelableManga(manga, withChapters = false)) + context.startService(intent) + } + + fun prefetchPages(context: Context, chapter: MangaChapter) { + if (!isPrefetchAvailable(context, chapter.source)) return + val intent = Intent(context, MangaPrefetchService::class.java) + intent.action = ACTION_PREFETCH_PAGES + intent.putExtra(EXTRA_CHAPTER, ParcelableMangaChapters(listOf(chapter))) + context.startService(intent) + } + + private fun isPrefetchAvailable(context: Context, source: MangaSource): Boolean { + if (source == MangaSource.LOCAL) { + return false + } + val entryPoint = EntryPointAccessors.fromApplication(context, PrefetchCompanionEntryPoint::class.java) + if (entryPoint.contentCache is StubContentCache) { + return false + } + return true + } + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/details/service/PrefetchCompanionEntryPoint.kt b/app/src/main/java/org/koitharu/kotatsu/details/service/PrefetchCompanionEntryPoint.kt new file mode 100644 index 000000000..57afeb770 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/details/service/PrefetchCompanionEntryPoint.kt @@ -0,0 +1,14 @@ +package org.koitharu.kotatsu.details.service + +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import org.koitharu.kotatsu.core.cache.ContentCache +import org.koitharu.kotatsu.core.prefs.AppSettings + +@EntryPoint +@InstallIn(SingletonComponent::class) +interface PrefetchCompanionEntryPoint { + val settings: AppSettings + val contentCache: ContentCache +} diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt index 91c534ea4..a7f66a2bc 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt @@ -18,6 +18,7 @@ import androidx.core.graphics.Insets import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.view.updatePadding +import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.BaseTransientBottomBar @@ -33,6 +34,8 @@ import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga import org.koitharu.kotatsu.core.os.ShortcutsUpdater import org.koitharu.kotatsu.core.ui.MangaErrorDialog import org.koitharu.kotatsu.databinding.ActivityDetailsBinding +import org.koitharu.kotatsu.details.service.MangaPrefetchService +import org.koitharu.kotatsu.details.ui.model.ChapterListItem import org.koitharu.kotatsu.details.ui.model.HistoryInfo import org.koitharu.kotatsu.download.ui.service.DownloadService import org.koitharu.kotatsu.main.ui.owners.NoModalBottomSheetOwner @@ -120,6 +123,7 @@ class DetailsActivity : viewModel.branches.observe(this) { binding.buttonDropdown.isVisible = it.size > 1 } + viewModel.chapters.observe(this, PrefetchObserver(this)) registerReceiver(downloadReceiver, IntentFilter(DownloadService.ACTION_DOWNLOAD_COMPLETE)) addMenuProvider( @@ -325,6 +329,24 @@ class DetailsActivity : return sb } + private class PrefetchObserver( + private val context: Context, + ) : Observer> { + + private var isCalled = false + + override fun onChanged(t: List?) { + if (t.isNullOrEmpty()) { + return + } + if (!isCalled) { + isCalled = true + val item = t.find { it.hasFlag(ChapterListItem.FLAG_CURRENT) } ?: t.first() + MangaPrefetchService.prefetchPages(context, item.chapter) + } + } + } + companion object { fun newIntent(context: Context, manga: Manga): Intent {