Prefetch content #291
1
app/proguard-rules.pro
vendored
1
app/proguard-rules.pro
vendored
@@ -14,3 +14,4 @@
|
||||
|
||||
-keep class org.koitharu.kotatsu.core.exceptions.* { *; }
|
||||
-keep class org.koitharu.kotatsu.settings.NotificationSettingsLegacyFragment
|
||||
-keep class org.koitharu.kotatsu.core.prefs.ScreenshotsPolicy { *; }
|
||||
|
||||
@@ -188,6 +188,9 @@
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/sync_history" />
|
||||
</service>
|
||||
<service
|
||||
android:name="org.koitharu.kotatsu.details.service.MangaPrefetchService"
|
||||
android:exported="false" />
|
||||
|
||||
<provider
|
||||
android:name="org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider"
|
||||
@@ -250,7 +253,7 @@
|
||||
android:value="true" />
|
||||
<meta-data
|
||||
android:name="com.samsung.android.icon_container.has_icon_container"
|
||||
android:value="@bool/com_samsung_android_icon_container_has_icon_container"/>
|
||||
android:value="@bool/com_samsung_android_icon_container_has_icon_container" />
|
||||
|
||||
</application>
|
||||
|
||||
|
||||
@@ -23,6 +23,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.os.NetworkState
|
||||
@@ -40,6 +43,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
|
||||
@@ -167,5 +171,17 @@ interface AppModule {
|
||||
activityRecreationHandle,
|
||||
incognitoModeIndicator,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideContentCache(
|
||||
application: Application,
|
||||
): ContentCache {
|
||||
return if (application.activityManager?.isLowRamDevice == true) {
|
||||
StubContentCache()
|
||||
} else {
|
||||
MemoryContentCache(application)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
23
app/src/main/java/org/koitharu/kotatsu/core/cache/ContentCache.kt
vendored
Normal file
23
app/src/main/java/org/koitharu/kotatsu/core/cache/ContentCache.kt
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
package org.koitharu.kotatsu.core.cache
|
||||
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
|
||||
interface ContentCache {
|
||||
|
||||
val isCachingEnabled: Boolean
|
||||
|
||||
suspend fun getDetails(source: MangaSource, url: String): Manga?
|
||||
|
||||
fun putDetails(source: MangaSource, url: String, details: SafeDeferred<Manga>)
|
||||
|
||||
suspend fun getPages(source: MangaSource, url: String): List<MangaPage>?
|
||||
|
||||
fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>)
|
||||
|
||||
data class Key(
|
||||
val source: MangaSource,
|
||||
val url: String,
|
||||
)
|
||||
}
|
||||
16
app/src/main/java/org/koitharu/kotatsu/core/cache/DeferredLruCache.kt
vendored
Normal file
16
app/src/main/java/org/koitharu/kotatsu/core/cache/DeferredLruCache.kt
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
package org.koitharu.kotatsu.core.cache
|
||||
|
||||
import androidx.collection.LruCache
|
||||
|
||||
class DeferredLruCache<T>(maxSize: Int) : LruCache<ContentCache.Key, SafeDeferred<T>>(maxSize) {
|
||||
|
||||
override fun entryRemoved(
|
||||
evicted: Boolean,
|
||||
key: ContentCache.Key,
|
||||
oldValue: SafeDeferred<T>,
|
||||
newValue: SafeDeferred<T>?,
|
||||
) {
|
||||
super.entryRemoved(evicted, key, oldValue, newValue)
|
||||
oldValue.cancel()
|
||||
}
|
||||
}
|
||||
59
app/src/main/java/org/koitharu/kotatsu/core/cache/MemoryContentCache.kt
vendored
Normal file
59
app/src/main/java/org/koitharu/kotatsu/core/cache/MemoryContentCache.kt
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
package org.koitharu.kotatsu.core.cache
|
||||
|
||||
import android.app.Application
|
||||
import android.content.ComponentCallbacks2
|
||||
import android.content.res.Configuration
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
|
||||
class MemoryContentCache(application: Application) : ContentCache, ComponentCallbacks2 {
|
||||
|
||||
init {
|
||||
application.registerComponentCallbacks(this)
|
||||
}
|
||||
|
||||
private val detailsCache = DeferredLruCache<Manga>(4)
|
||||
private val pagesCache = DeferredLruCache<List<MangaPage>>(4)
|
||||
|
||||
override val isCachingEnabled: Boolean = true
|
||||
|
||||
override suspend fun getDetails(source: MangaSource, url: String): Manga? {
|
||||
return detailsCache[ContentCache.Key(source, url)]?.awaitOrNull()
|
||||
}
|
||||
|
||||
override fun putDetails(source: MangaSource, url: String, details: SafeDeferred<Manga>) {
|
||||
detailsCache.put(ContentCache.Key(source, url), details)
|
||||
}
|
||||
|
||||
override suspend fun getPages(source: MangaSource, url: String): List<MangaPage>? {
|
||||
return pagesCache[ContentCache.Key(source, url)]?.awaitOrNull()
|
||||
}
|
||||
|
||||
override fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>) {
|
||||
pagesCache.put(ContentCache.Key(source, url), pages)
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) = Unit
|
||||
|
||||
override fun onLowMemory() = Unit
|
||||
|
||||
override fun onTrimMemory(level: Int) {
|
||||
trimCache(detailsCache, level)
|
||||
trimCache(pagesCache, level)
|
||||
}
|
||||
|
||||
private fun trimCache(cache: DeferredLruCache<*>, level: Int) {
|
||||
when (level) {
|
||||
ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL,
|
||||
ComponentCallbacks2.TRIM_MEMORY_COMPLETE,
|
||||
ComponentCallbacks2.TRIM_MEMORY_MODERATE -> cache.evictAll()
|
||||
|
||||
ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN,
|
||||
ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW,
|
||||
ComponentCallbacks2.TRIM_MEMORY_BACKGROUND -> cache.trimToSize(1)
|
||||
|
||||
else -> cache.trimToSize(cache.maxSize() / 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
20
app/src/main/java/org/koitharu/kotatsu/core/cache/SafeDeferred.kt
vendored
Normal file
20
app/src/main/java/org/koitharu/kotatsu/core/cache/SafeDeferred.kt
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
package org.koitharu.kotatsu.core.cache
|
||||
|
||||
import kotlinx.coroutines.Deferred
|
||||
|
||||
class SafeDeferred<T>(
|
||||
private val delegate: Deferred<Result<T>>,
|
||||
) {
|
||||
|
||||
suspend fun await(): T {
|
||||
return delegate.await().getOrThrow()
|
||||
}
|
||||
|
||||
suspend fun awaitOrNull(): T? {
|
||||
return delegate.await().getOrNull()
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
delegate.cancel()
|
||||
}
|
||||
}
|
||||
18
app/src/main/java/org/koitharu/kotatsu/core/cache/StubContentCache.kt
vendored
Normal file
18
app/src/main/java/org/koitharu/kotatsu/core/cache/StubContentCache.kt
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
package org.koitharu.kotatsu.core.cache
|
||||
|
||||
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 val isCachingEnabled: Boolean = false
|
||||
|
||||
override suspend fun getDetails(source: MangaSource, url: String): Manga? = null
|
||||
|
||||
override fun putDetails(source: MangaSource, url: String, details: SafeDeferred<Manga>) = Unit
|
||||
|
||||
override suspend fun getPages(source: MangaSource, url: String): List<MangaPage>? = null
|
||||
|
||||
override fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>) = Unit
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,12 +1,28 @@
|
||||
package org.koitharu.kotatsu.core.parser
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import org.koitharu.kotatsu.core.cache.ContentCache
|
||||
import org.koitharu.kotatsu.core.cache.SafeDeferred
|
||||
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
|
||||
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
|
||||
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
||||
|
||||
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 +44,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 }
|
||||
val details = asyncSafe {
|
||||
parser.getDetails(manga)
|
||||
}
|
||||
cache.putDetails(source, manga.url, details)
|
||||
return 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 }
|
||||
val pages = asyncSafe {
|
||||
parser.getPages(chapter)
|
||||
}
|
||||
cache.putPages(source, chapter.url, pages)
|
||||
return pages.await()
|
||||
}
|
||||
|
||||
override suspend fun getPageUrl(page: MangaPage): String = parser.getPageUrl(page)
|
||||
|
||||
@@ -45,4 +75,10 @@ class RemoteMangaRepository(private val parser: MangaParser) : MangaRepository {
|
||||
}
|
||||
|
||||
private fun getConfig() = parser.config as SourceSettings
|
||||
}
|
||||
|
||||
private fun <T> asyncSafe(block: suspend CoroutineScope.() -> T) = SafeDeferred(
|
||||
processLifecycleScope.async(Dispatchers.Default) {
|
||||
runCatchingCancellable { block() }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package org.koitharu.kotatsu.core.prefs
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Uri
|
||||
import android.provider.Settings
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
@@ -17,6 +16,7 @@ import org.koitharu.kotatsu.core.network.DoHProvider
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.shelf.domain.ShelfSection
|
||||
import org.koitharu.kotatsu.utils.ext.connectivityManager
|
||||
import org.koitharu.kotatsu.utils.ext.getEnumValue
|
||||
import org.koitharu.kotatsu.utils.ext.observe
|
||||
import org.koitharu.kotatsu.utils.ext.putEnumValue
|
||||
@@ -34,6 +34,7 @@ import javax.inject.Singleton
|
||||
class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
|
||||
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
private val connectivityManager = context.connectivityManager
|
||||
|
||||
private val remoteSources = EnumSet.allOf(MangaSource::class.java).apply {
|
||||
remove(MangaSource.LOCAL)
|
||||
@@ -156,6 +157,11 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
val isDynamicShortcutsEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_SHORTCUTS, true)
|
||||
|
||||
fun isContentPrefetchEnabled(): Boolean {
|
||||
val policy = NetworkPolicy.from(prefs.getString(KEY_PREFETCH_CONTENT, null), NetworkPolicy.NEVER)
|
||||
return policy.isNetworkAllowed(connectivityManager)
|
||||
}
|
||||
|
||||
var sourcesOrder: List<String>
|
||||
get() = prefs.getString(KEY_SOURCES_ORDER, null)
|
||||
?.split('|')
|
||||
@@ -234,12 +240,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
val isWebtoonZoomEnable: Boolean
|
||||
get() = prefs.getBoolean(KEY_WEBTOON_ZOOM, true)
|
||||
|
||||
fun isPagesPreloadAllowed(cm: ConnectivityManager): Boolean {
|
||||
return when (prefs.getString(KEY_PAGES_PRELOAD, null)?.toIntOrNull()) {
|
||||
NETWORK_ALWAYS -> true
|
||||
NETWORK_NEVER -> false
|
||||
else -> cm.isActiveNetworkMetered
|
||||
}
|
||||
fun isPagesPreloadEnabled(): Boolean {
|
||||
val policy = NetworkPolicy.from(prefs.getString(KEY_PAGES_PRELOAD, null), NetworkPolicy.NON_METERED)
|
||||
return policy.isNetworkAllowed(connectivityManager)
|
||||
}
|
||||
|
||||
fun getDateFormat(format: String = prefs.getString(KEY_DATE_FORMAT, "").orEmpty()): DateFormat =
|
||||
@@ -293,7 +296,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val TRACK_FAVOURITES = "favourites"
|
||||
|
||||
const val KEY_LIST_MODE = "list_mode_2"
|
||||
const val KEY_APP_SECTION = "app_section_2"
|
||||
const val KEY_THEME = "theme"
|
||||
const val KEY_DYNAMIC_THEME = "dynamic_theme"
|
||||
const val KEY_THEME_AMOLED = "amoled_theme"
|
||||
@@ -355,13 +357,10 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val KEY_LOCAL_LIST_ORDER = "local_order"
|
||||
const val KEY_WEBTOON_ZOOM = "webtoon_zoom"
|
||||
const val KEY_SHELF_SECTIONS = "shelf_sections_2"
|
||||
const val KEY_PREFETCH_CONTENT = "prefetch_content"
|
||||
|
||||
// About
|
||||
const val KEY_APP_UPDATE = "app_update"
|
||||
const val KEY_APP_TRANSLATION = "about_app_translation"
|
||||
|
||||
private const val NETWORK_NEVER = 0
|
||||
private const val NETWORK_ALWAYS = 1
|
||||
private const val NETWORK_NON_METERED = 2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package org.koitharu.kotatsu.core.prefs
|
||||
|
||||
import android.net.ConnectivityManager
|
||||
|
||||
enum class NetworkPolicy(
|
||||
private val key: Int,
|
||||
) {
|
||||
|
||||
NEVER(0),
|
||||
ALWAYS(1),
|
||||
NON_METERED(2);
|
||||
|
||||
fun isNetworkAllowed(cm: ConnectivityManager) = when (this) {
|
||||
NEVER -> false
|
||||
ALWAYS -> true
|
||||
NON_METERED -> !cm.isActiveNetworkMetered
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun from(key: String?, default: NetworkPolicy): NetworkPolicy {
|
||||
val intKey = key?.toIntOrNull() ?: return default
|
||||
return enumValues<NetworkPolicy>().find { it.key == intKey } ?: default
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
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.coroutineScope
|
||||
import org.koitharu.kotatsu.base.ui.CoroutineIntentService
|
||||
import org.koitharu.kotatsu.core.cache.ContentCache
|
||||
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.history.domain.HistoryRepository
|
||||
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.runCatchingCancellable
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MangaPrefetchService : CoroutineIntentService() {
|
||||
|
||||
@Inject
|
||||
lateinit var mangaRepositoryFactory: MangaRepository.Factory
|
||||
|
||||
@Inject
|
||||
lateinit var cache: ContentCache
|
||||
|
||||
@Inject
|
||||
lateinit var historyRepository: HistoryRepository
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
ACTION_PREFETCH_LAST -> prefetchLast()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(startId: Int, error: Throwable) = Unit
|
||||
|
||||
private suspend fun prefetchDetails(manga: Manga) = coroutineScope {
|
||||
val source = mangaRepositoryFactory.create(manga.source)
|
||||
runCatchingCancellable { source.getDetails(manga) }
|
||||
}
|
||||
|
||||
private suspend fun prefetchPages(chapter: MangaChapter) {
|
||||
val source = mangaRepositoryFactory.create(chapter.source)
|
||||
runCatchingCancellable { source.getPages(chapter) }
|
||||
}
|
||||
|
||||
private suspend fun prefetchLast() {
|
||||
val last = historyRepository.getLastOrNull() ?: return
|
||||
if (last.source == MangaSource.LOCAL) return
|
||||
val repo = mangaRepositoryFactory.create(last.source)
|
||||
val details = runCatchingCancellable { repo.getDetails(last) }.getOrNull() ?: return
|
||||
val chapters = details.chapters
|
||||
if (chapters.isNullOrEmpty()) {
|
||||
return
|
||||
}
|
||||
val history = historyRepository.getOne(last)
|
||||
val chapter = if (history == null) {
|
||||
chapters.firstOrNull()
|
||||
} else {
|
||||
chapters.find { x -> x.id == history.chapterId } ?: chapters.firstOrNull()
|
||||
} ?: return
|
||||
runCatchingCancellable { repo.getPages(chapter) }
|
||||
}
|
||||
|
||||
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"
|
||||
private const val ACTION_PREFETCH_LAST = "last"
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
fun prefetchLast(context: Context) {
|
||||
if (!isPrefetchAvailable(context, null)) return
|
||||
val intent = Intent(context, MangaPrefetchService::class.java)
|
||||
intent.action = ACTION_PREFETCH_LAST
|
||||
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)
|
||||
return entryPoint.contentCache.isCachingEnabled && entryPoint.settings.isContentPrefetchEnabled()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -41,6 +41,7 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.base.ui.widgets.SlidingBottomNavigationView
|
||||
import org.koitharu.kotatsu.databinding.ActivityMainBinding
|
||||
import org.koitharu.kotatsu.details.service.MangaPrefetchService
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||
import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
|
||||
@@ -334,6 +335,7 @@ class MainActivity :
|
||||
TrackWorker.setup(applicationContext)
|
||||
SuggestionsWorker.setup(applicationContext)
|
||||
}
|
||||
MangaPrefetchService.prefetchLast(this@MainActivity)
|
||||
requestNotificationsPermission()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package org.koitharu.kotatsu.reader.domain
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import androidx.collection.LongSparseArray
|
||||
import androidx.collection.set
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -31,7 +29,6 @@ import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.await
|
||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||
import org.koitharu.kotatsu.utils.ext.connectivityManager
|
||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.utils.ext.withProgress
|
||||
import org.koitharu.kotatsu.utils.progress.ProgressDeferred
|
||||
@@ -50,13 +47,11 @@ class PageLoader @Inject constructor(
|
||||
private val okHttp: OkHttpClient,
|
||||
private val cache: PagesCache,
|
||||
private val settings: AppSettings,
|
||||
@ApplicationContext context: Context,
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
) : Closeable {
|
||||
|
||||
val loaderScope = CoroutineScope(SupervisorJob() + InternalErrorHandler() + Dispatchers.Default)
|
||||
|
||||
private val connectivityManager = context.connectivityManager
|
||||
private val tasks = LongSparseArray<ProgressDeferred<File, Float>>()
|
||||
private val convertLock = Mutex()
|
||||
private var repository: MangaRepository? = null
|
||||
@@ -73,7 +68,7 @@ class PageLoader @Inject constructor(
|
||||
}
|
||||
|
||||
fun isPrefetchApplicable(): Boolean {
|
||||
return repository is RemoteMangaRepository && settings.isPagesPreloadAllowed(connectivityManager)
|
||||
return repository is RemoteMangaRepository && settings.isPagesPreloadEnabled()
|
||||
}
|
||||
|
||||
fun prefetch(pages: List<ReaderPage>) {
|
||||
|
||||
@@ -9,14 +9,13 @@ import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.base.ui.dialog.StorageSelectDialog
|
||||
import org.koitharu.kotatsu.core.cache.ContentCache
|
||||
import org.koitharu.kotatsu.core.network.DoHProvider
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
||||
@@ -26,6 +25,8 @@ import org.koitharu.kotatsu.sync.ui.SyncSettingsIntent
|
||||
import org.koitharu.kotatsu.utils.ext.getStorageName
|
||||
import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat
|
||||
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ContentSettingsFragment :
|
||||
@@ -36,9 +37,12 @@ class ContentSettingsFragment :
|
||||
@Inject
|
||||
lateinit var storageManager: LocalStorageManager
|
||||
|
||||
@Inject
|
||||
lateinit var contentCache: ContentCache
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_content)
|
||||
|
||||
findPreference<Preference>(AppSettings.KEY_PREFETCH_CONTENT)?.isVisible = contentCache.isCachingEnabled
|
||||
findPreference<SliderPreference>(AppSettings.KEY_DOWNLOADS_PARALLELISM)?.run {
|
||||
summary = value.toString()
|
||||
setOnPreferenceChangeListener { preference, newValue ->
|
||||
@@ -82,11 +86,13 @@ class ContentSettingsFragment :
|
||||
AppSettings.KEY_LOCAL_STORAGE -> {
|
||||
findPreference<Preference>(key)?.bindStorageName()
|
||||
}
|
||||
|
||||
AppSettings.KEY_SUGGESTIONS -> {
|
||||
findPreference<Preference>(AppSettings.KEY_SUGGESTIONS)?.setSummary(
|
||||
if (settings.isSuggestionsEnabled) R.string.enabled else R.string.disabled,
|
||||
)
|
||||
}
|
||||
|
||||
AppSettings.KEY_SOURCES_HIDDEN -> {
|
||||
bindRemoteSourcesSummary()
|
||||
}
|
||||
@@ -104,6 +110,7 @@ class ContentSettingsFragment :
|
||||
.show()
|
||||
true
|
||||
}
|
||||
|
||||
AppSettings.KEY_SYNC -> {
|
||||
val am = AccountManager.get(requireContext())
|
||||
val accountType = getString(R.string.account_type_sync)
|
||||
@@ -119,6 +126,7 @@ class ContentSettingsFragment :
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,4 +399,5 @@
|
||||
<string name="clear_new_chapters_counters">Also clear information about new chapters</string>
|
||||
<string name="compact">Compact</string>
|
||||
<string name="source_disabled">Source disabled</string>
|
||||
<string name="prefetch_content">Content preloading</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.koitharu.kotatsu.settings.sources.SourcesSettingsFragment"
|
||||
@@ -19,6 +20,16 @@
|
||||
android:title="@string/dns_over_https"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="0"
|
||||
android:entries="@array/network_policy"
|
||||
android:entryValues="@array/values_network_policy"
|
||||
android:key="prefetch_content"
|
||||
android:title="@string/prefetch_content"
|
||||
app:isPreferenceVisible="false"
|
||||
app:useSimpleSummaryProvider="true"
|
||||
tools:isPreferenceVisible="true" />
|
||||
|
||||
<Preference
|
||||
android:key="local_storage"
|
||||
android:persistent="false"
|
||||
@@ -50,4 +61,4 @@
|
||||
android:fragment="org.koitharu.kotatsu.settings.backup.BackupSettingsFragment"
|
||||
android:title="@string/backup_restore" />
|
||||
|
||||
</PreferenceScreen>
|
||||
</PreferenceScreen>
|
||||
|
||||
@@ -61,20 +61,19 @@
|
||||
android:title="@string/show_pages_numbers" />
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="allow"
|
||||
android:entries="@array/screenshots_policy"
|
||||
android:entryValues="@array/values_screenshots_policy"
|
||||
android:key="screenshots_policy"
|
||||
android:title="@string/screenshots_policy"
|
||||
app:defaultValue="allow"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="2"
|
||||
android:entries="@array/network_policy"
|
||||
android:entryValues="@array/values_network_policy"
|
||||
android:key="pages_preload"
|
||||
android:title="@string/preload_pages"
|
||||
app:defaultValue="2"
|
||||
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
</PreferenceScreen>
|
||||
|
||||
Reference in New Issue
Block a user