Prefetch chapter

This commit is contained in:
Koitharu
2023-01-04 07:40:42 +02:00
parent 8d9426f257
commit e6ae9e8bd6
11 changed files with 280 additions and 11 deletions

View File

@@ -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()
}
}
}
}

View File

@@ -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<Manga>)
suspend fun getPages(source: MangaSource, url: String): List<MangaPage>?
fun putPages(source: MangaSource, url: String, pages: Deferred<List<MangaPage>>)
data class Key(
val source: MangaSource,
val url: String,
)
}

View File

@@ -0,0 +1,17 @@
package org.koitharu.kotatsu.core.cache
import androidx.collection.LruCache
import kotlinx.coroutines.Deferred
class DeferredLruCache<T>(maxSize: Int) : LruCache<ContentCache.Key, Deferred<T>>(maxSize) {
override fun entryRemoved(
evicted: Boolean,
key: ContentCache.Key,
oldValue: Deferred<T>,
newValue: Deferred<T>?,
) {
super.entryRemoved(evicted, key, oldValue, newValue)
oldValue.cancel()
}
}

View File

@@ -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<Manga>(10)
private val pagesCache = DeferredLruCache<List<MangaPage>>(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<Manga>) {
detailsCache.put(ContentCache.Key(source, url), details)
}
override suspend fun getPages(source: MangaSource, url: String): List<MangaPage>? {
return pagesCache[ContentCache.Key(source, url)]?.await()
}
override fun putPages(source: MangaSource, url: String, pages: Deferred<List<MangaPage>>) {
pagesCache.put(ContentCache.Key(source, url), pages)
}
}

View File

@@ -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<Manga>) = Unit
override suspend fun getPages(source: MangaSource, url: String): List<MangaPage>? = null
override fun putPages(source: MangaSource, url: String, pages: Deferred<List<MangaPage>>) = Unit
}

View File

@@ -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, WeakReference<RemoteMangaRepository>>(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
}

View File

@@ -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<MangaPage> = parser.getPages(chapter)
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
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
}
}

View File

@@ -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<ParcelableManga>(EXTRA_MANGA)?.manga ?: return,
)
ACTION_PREFETCH_PAGES -> prefetchPages(
chapter = intent.getParcelableExtraCompat<ParcelableMangaChapters>(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
}
}
}

View File

@@ -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
}

View File

@@ -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<List<ChapterListItem>> {
private var isCalled = false
override fun onChanged(t: List<ChapterListItem>?) {
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 {