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 {