Coil updates

Non library manga goes into android's default cache
Manga Fetcher now respects network cache policy
Not showing error drawable on manga details
Fixed manga notification icon sometimes not showing
General cleanup
This commit is contained in:
Jay 2020-05-16 23:54:43 -04:00
parent 67a76da475
commit cce6fdb765
12 changed files with 118 additions and 74 deletions

View File

@ -13,7 +13,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.Cache
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.File import java.io.File
@ -36,8 +35,6 @@ class CoverCache(val context: Context) {
private val cacheDir = context.getExternalFilesDir("covers") private val cacheDir = context.getExternalFilesDir("covers")
?: File(context.filesDir, "covers").also { it.mkdirs() } ?: File(context.filesDir, "covers").also { it.mkdirs() }
val cache = Cache(cacheDir, 300 * 1024 * 1024) // 300MB
fun getChapterCacheSize(): String { fun getChapterCacheSize(): String {
return Formatter.formatFileSize(context, DiskUtil.getDirectorySize(cacheDir)) return Formatter.formatFileSize(context, DiskUtil.getDirectorySize(cacheDir))
} }

View File

@ -21,7 +21,6 @@ class ByteArrayFetcher : Fetcher<ByteArray> {
size: Size, size: Size,
options: Options options: Options
): FetchResult { ): FetchResult {
val source = ByteArrayInputStream(data).source().buffer()
return SourceResult( return SourceResult(
source = ByteArrayInputStream(data).source().buffer(), source = ByteArrayInputStream(data).source().buffer(),
mimeType = "image/gif", mimeType = "image/gif",

View File

@ -9,7 +9,6 @@ import coil.decode.ImageDecoderDecoder
import coil.decode.SvgDecoder import coil.decode.SvgDecoder
import coil.util.CoilUtils import coil.util.CoilUtils
import com.chuckerteam.chucker.api.ChuckerInterceptor import com.chuckerteam.chucker.api.ChuckerInterceptor
import eu.kanade.tachiyomi.R
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
class CoilSetup(context: Context) { class CoilSetup(context: Context) {
@ -19,7 +18,6 @@ class CoilSetup(context: Context) {
.crossfade(true) .crossfade(true)
.allowRgb565(true) .allowRgb565(true)
.allowHardware(false) .allowHardware(false)
.error(R.drawable.ic_broken_image_grey_24dp)
.componentRegistry { .componentRegistry {
if (Build.VERSION.SDK_INT >= 28) { if (Build.VERSION.SDK_INT >= 28) {
add(ImageDecoderDecoder()) add(ImageDecoderDecoder())

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.data.download.coil package eu.kanade.tachiyomi.data.download.coil
import android.webkit.MimeTypeMap
import coil.bitmappool.BitmapPool import coil.bitmappool.BitmapPool
import coil.decode.DataSource import coil.decode.DataSource
import coil.decode.Options import coil.decode.Options
@ -10,11 +11,15 @@ import coil.size.Size
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil
import okhttp3.CacheControl
import okhttp3.Call import okhttp3.Call
import okhttp3.Request import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody
import okio.buffer import okio.buffer
import okio.sink import okio.sink
import okio.source import okio.source
@ -29,55 +34,95 @@ class MangaFetcher() : Fetcher<Manga> {
private val sourceManager: SourceManager by injectLazy() private val sourceManager: SourceManager by injectLazy()
private val defaultClient = Injekt.get<NetworkHelper>().client private val defaultClient = Injekt.get<NetworkHelper>().client
override fun key(manga: Manga): String? { override fun key(data: Manga): String? {
if (manga.thumbnail_url.isNullOrBlank()) return null if (data.thumbnail_url.isNullOrBlank()) return null
return DiskUtil.hashKeyForDisk(manga.thumbnail_url!!) return if (!data.favorite) {
data.thumbnail_url!!
} else {
DiskUtil.hashKeyForDisk(data.thumbnail_url!!)
}
} }
override suspend fun fetch(pool: BitmapPool, manga: Manga, size: Size, options: Options): FetchResult { override suspend fun fetch(pool: BitmapPool, data: Manga, size: Size, options: Options): FetchResult {
val cover = manga.thumbnail_url val cover = data.thumbnail_url
when (getResourceType(cover)) { return when (getResourceType(cover)) {
Type.File -> { Type.File -> fileLoader(data)
return fileLoader(manga) Type.URL -> httpLoader(data, options)
} Type.CUSTOM -> customLoader(data, options)
Type.URL -> {
return httpLoader(manga)
}
Type.CUSTOM -> {
return customLoader(manga)
}
null -> error("Invalid image") null -> error("Invalid image")
} }
} }
private fun customLoader(manga: Manga): FetchResult { private suspend fun customLoader(manga: Manga, options: Options): FetchResult {
val coverFile = coverCache.getCoverFile(manga) val coverFile = coverCache.getCoverFile(manga)
if (coverFile.exists()) { if (coverFile.exists()) {
return fileLoader(coverFile) return fileLoader(coverFile)
} }
manga.thumbnail_url = manga.thumbnail_url!!.substringAfter("-J2K-").substringAfter("CUSTOM-") manga.thumbnail_url = manga.thumbnail_url!!.substringAfter("-J2K-").substringAfter("CUSTOM-")
return httpLoader(manga) return httpLoader(manga, options)
} }
private fun httpLoader(manga: Manga): FetchResult { private suspend fun httpLoader(manga: Manga, options: Options): FetchResult {
val coverFile = coverCache.getCoverFile(manga) val coverFile = coverCache.getCoverFile(manga)
if (coverFile.exists()) { if (coverFile.exists()) {
return fileLoader(coverFile) return fileLoader(coverFile)
} }
val call = getCall(manga) if (!manga.favorite) {
val tmpFile = File(coverFile.absolutePath + "_tmp") val (response, body) = awaitGetCall(manga)
val response = call.execute() return SourceResult(
val body = checkNotNull(response.body) { "Null response source" } source = body.source(),
mimeType = getMimeType(manga.thumbnail_url!!, body),
dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK
)
} else {
val (_, body) = awaitGetCall(manga, !options.networkCachePolicy.readEnabled)
body.source().use { input -> val tmpFile = File(coverFile.absolutePath + "_tmp")
tmpFile.sink().buffer().use { output -> body.source().use { input ->
output.writeAll(input) tmpFile.sink().buffer().use { output ->
output.writeAll(input)
}
} }
tmpFile.renameTo(coverFile)
return fileLoader(coverFile)
}
}
private suspend fun awaitGetCall(manga: Manga, onlyCache: Boolean = false): Pair<Response,
ResponseBody> {
val call = getCall(manga, onlyCache)
val response = call.await()
return response to checkNotNull(response.body) { "Null response source" }
}
/**
* "text/plain" is often used as a default/fallback MIME type.
* Attempt to guess a better MIME type from the file extension.
*/
private fun getMimeType(data: String, body: ResponseBody): String? {
val rawContentType = body.contentType()?.toString()
return if (rawContentType == null || rawContentType.startsWith("text/plain")) {
MimeTypeMap.getSingleton().getMimeTypeFromUrl(data) ?: rawContentType
} else {
rawContentType
}
}
/** Modified from [MimeTypeMap.getFileExtensionFromUrl] to be more permissive with special characters. */
private fun MimeTypeMap.getMimeTypeFromUrl(url: String?): String? {
if (url.isNullOrBlank()) {
return null
} }
tmpFile.renameTo(coverFile) val extension = url
return fileLoader(coverFile) .substringBeforeLast('#') // Strip the fragment.
.substringBeforeLast('?') // Strip the query.
.substringAfterLast('/') // Get the last path segment.
.substringAfterLast('.', missingDelimiterValue = "") // Get the file extension.
return getMimeTypeFromExtension(extension)
} }
private fun fileLoader(manga: Manga): FetchResult { private fun fileLoader(manga: Manga): FetchResult {
@ -92,18 +137,19 @@ class MangaFetcher() : Fetcher<Manga> {
) )
} }
private fun getCall(manga: Manga): Call { private fun getCall(manga: Manga, onlyCache: Boolean): Call {
val source = sourceManager.get(manga.source) as? HttpSource val source = sourceManager.get(manga.source) as? HttpSource
val client = source?.client ?: defaultClient val client = source?.client ?: defaultClient
val newClient = client.newBuilder() val newClient = client.newBuilder().build()
.cache(coverCache.cache)
.build()
val request = Request.Builder().url(manga.thumbnail_url!!).also { val request = Request.Builder().url(manga.thumbnail_url!!).also {
if (source != null) { if (source != null) {
it.headers(source.headers) it.headers(source.headers)
} }
if (onlyCache) {
it.cacheControl(CacheControl.FORCE_CACHE)
}
}.build() }.build()
return newClient.newCall(request) return newClient.newCall(request)

View File

@ -16,6 +16,7 @@ import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import coil.Coil import coil.Coil
import coil.request.CachePolicy import coil.request.CachePolicy
import coil.request.GetRequest
import coil.request.LoadRequest import coil.request.LoadRequest
import coil.transform.CircleCropTransformation import coil.transform.CircleCropTransformation
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@ -603,7 +604,7 @@ class LibraryUpdateService(
* *
* @param updates a list of manga with new updates. * @param updates a list of manga with new updates.
*/ */
private fun showResultNotification(updates: Map<LibraryManga, Array<Chapter>>) { private suspend fun showResultNotification(updates: Map<LibraryManga, Array<Chapter>>) {
val notifications = ArrayList<Pair<Notification, Int>>() val notifications = ArrayList<Pair<Notification, Int>>()
updates.forEach { updates.forEach {
val manga = it.key val manga = it.key
@ -613,11 +614,15 @@ class LibraryUpdateService(
setSmallIcon(R.drawable.ic_tachi) setSmallIcon(R.drawable.ic_tachi)
try { try {
val request = LoadRequest.Builder(this@LibraryUpdateService).data(manga) val request = GetRequest.Builder(this@LibraryUpdateService).data(manga)
.transformations(CircleCropTransformation()).size(width = 256, height = 256) .networkCachePolicy(CachePolicy.DISABLED)
.target { drawable -> setLargeIcon((drawable as BitmapDrawable).bitmap) }.build() .transformations(CircleCropTransformation()).size(width = 256, height = 256)
.build()
Coil.imageLoader(this@LibraryUpdateService).execute(request) Coil.imageLoader(this@LibraryUpdateService)
.execute(request).drawable?.let { drawable ->
setLargeIcon((drawable as BitmapDrawable).bitmap)
}
} catch (e: Exception) { } catch (e: Exception) {
} }
setGroupAlertBehavior(GROUP_ALERT_SUMMARY) setGroupAlertBehavior(GROUP_ALERT_SUMMARY)

View File

@ -4,7 +4,6 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import coil.api.clear import coil.api.clear
import coil.api.loadAny import coil.api.loadAny
import coil.transform.RoundedCornersTransformation
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.system.dpToPx
import eu.kanade.tachiyomi.util.view.gone import eu.kanade.tachiyomi.util.view.gone
@ -12,9 +11,7 @@ import eu.kanade.tachiyomi.util.view.updateLayoutParams
import eu.kanade.tachiyomi.util.view.visible import eu.kanade.tachiyomi.util.view.visible
import eu.kanade.tachiyomi.util.view.visibleIf import eu.kanade.tachiyomi.util.view.visibleIf
import kotlinx.android.synthetic.main.manga_list_item.* import kotlinx.android.synthetic.main.manga_list_item.*
import kotlinx.android.synthetic.main.manga_list_item.title
import kotlinx.android.synthetic.main.manga_list_item.view.* import kotlinx.android.synthetic.main.manga_list_item.view.*
import kotlinx.android.synthetic.main.recently_read_item.*
import kotlinx.android.synthetic.main.unread_download_badge.* import kotlinx.android.synthetic.main.unread_download_badge.*
/** /**
@ -82,9 +79,7 @@ class LibraryListHolder(
cover_thumbnail.clear() cover_thumbnail.clear()
} else { } else {
val id = item.manga.id ?: return val id = item.manga.id ?: return
cover_thumbnail.loadAny(item.manga) { cover_thumbnail.loadAny(item.manga)
transformations(RoundedCornersTransformation(2f, 2f, 2f, 2f))
}
} }
} }

View File

@ -39,8 +39,6 @@ import androidx.transition.ChangeImageTransform
import androidx.transition.TransitionManager import androidx.transition.TransitionManager
import androidx.transition.TransitionSet import androidx.transition.TransitionSet
import coil.Coil import coil.Coil
import coil.api.clear
import coil.api.loadAny
import coil.request.LoadRequest import coil.request.LoadRequest
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.checkbox.checkBoxPrompt import com.afollestad.materialdialogs.checkbox.checkBoxPrompt
@ -308,7 +306,6 @@ class MangaDetailsController : BaseController,
/** Get the color of the manga cover*/ /** Get the color of the manga cover*/
fun setPaletteColor() { fun setPaletteColor() {
val view = view ?: return val view = view ?: return
coverColor = null
val request = LoadRequest.Builder(view.context).data(manga).allowHardware(false) val request = LoadRequest.Builder(view.context).data(manga).allowHardware(false)
.target { drawable -> .target { drawable ->
@ -330,16 +327,12 @@ class MangaDetailsController : BaseController,
activity?.window?.statusBarColor = translucentColor activity?.window?.statusBarColor = translucentColor
} }
} }
manga_cover_full.setImageDrawable(drawable)
getHeader()?.updateCover(manga!!)
}.build() }.build()
Coil.imageLoader(view.context).execute(request) Coil.imageLoader(view.context).execute(request)
} }
fun resetCovers() {
manga_cover_full.clear()
manga_cover_full.loadAny(manga)
getHeader()?.updateCover(manga!!, true)
}
/** Set toolbar theme for themes that are inverted (ie. light blue theme) */ /** Set toolbar theme for themes that are inverted (ie. light blue theme) */
private fun setActionBar(forThis: Boolean) { private fun setActionBar(forThis: Boolean) {
val activity = activity ?: return val activity = activity ?: return
@ -1258,7 +1251,6 @@ class MangaDetailsController : BaseController,
currentAnimator?.cancel() currentAnimator?.cancel()
// Load the high-resolution "zoomed-in" image. // Load the high-resolution "zoomed-in" image.
manga_cover_full?.loadAny(manga)
val expandedImageView = manga_cover_full ?: return val expandedImageView = manga_cover_full ?: return
val fullBackdrop = full_backdrop val fullBackdrop = full_backdrop

View File

@ -748,7 +748,6 @@ class MangaDetailsPresenter(
fun forceUpdateCovers(deleteCache: Boolean = true) { fun forceUpdateCovers(deleteCache: Boolean = true) {
if (deleteCache) coverCache.deleteFromCache(manga) if (deleteCache) coverCache.deleteFromCache(manga)
controller.setPaletteColor() controller.setPaletteColor()
controller.resetCovers()
} }
fun editCoverWithStream(uri: Uri): Boolean { fun editCoverWithStream(uri: Uri): Boolean {

View File

@ -9,8 +9,8 @@ import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils import androidx.core.graphics.ColorUtils
import coil.api.clear
import coil.api.loadAny import coil.api.loadAny
import coil.request.CachePolicy
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
@ -308,12 +308,19 @@ class MangaHeaderHolder(
} }
} }
fun updateCover(manga: Manga, forceUpdate: Boolean = false) { fun updateCover(manga: Manga) {
if (!isCached(manga) && !forceUpdate) return if (!manga.initialized) return
manga_cover.clear() val drawable = adapter.controller.manga_cover_full?.drawable
backdrop.clear() manga_cover.loadAny(manga, builder = {
manga_cover.loadAny(manga) placeholder(drawable)
backdrop.loadAny(manga) error(drawable)
if (manga.favorite) networkCachePolicy(CachePolicy.DISABLED)
})
backdrop.loadAny(manga, builder = {
placeholder(drawable)
error(drawable)
if (manga.favorite) networkCachePolicy(CachePolicy.DISABLED)
})
} }
private fun isCached(manga: Manga): Boolean { private fun isCached(manga: Manga): Boolean {

View File

@ -13,8 +13,6 @@ import eu.kanade.tachiyomi.ui.library.LibraryCategoryAdapter
import eu.kanade.tachiyomi.util.view.gone import eu.kanade.tachiyomi.util.view.gone
import eu.kanade.tachiyomi.widget.CoverViewTarget import eu.kanade.tachiyomi.widget.CoverViewTarget
import kotlinx.android.synthetic.main.manga_grid_item.* import kotlinx.android.synthetic.main.manga_grid_item.*
import kotlinx.android.synthetic.main.manga_grid_item.cover_thumbnail
import kotlinx.android.synthetic.main.manga_grid_item.title
import kotlinx.android.synthetic.main.unread_download_badge.* import kotlinx.android.synthetic.main.unread_download_badge.*
/** /**
@ -63,7 +61,8 @@ class BrowseSourceGridHolder(
cover_thumbnail.clear() cover_thumbnail.clear()
} else { } else {
val id = manga.id ?: return val id = manga.id ?: return
val request = LoadRequest.Builder(view.context).data(manga).target(CoverViewTarget(cover_thumbnail, progress)).build() val request = LoadRequest.Builder(view.context).data(manga)
.target(CoverViewTarget(cover_thumbnail, progress)).build()
Coil.imageLoader(view.context).execute(request) Coil.imageLoader(view.context).execute(request)
} }
} }

View File

@ -5,7 +5,6 @@ import androidx.recyclerview.widget.RecyclerView
import coil.Coil import coil.Coil
import coil.api.clear import coil.api.clear
import coil.request.LoadRequest import coil.request.LoadRequest
import coil.transform.RoundedCornersTransformation
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@ -48,8 +47,8 @@ class BrowseSourceListHolder(private val view: View, adapter: FlexibleAdapter<IF
cover_thumbnail.clear() cover_thumbnail.clear()
} else { } else {
val id = manga.id ?: return val id = manga.id ?: return
val request = LoadRequest.Builder(view.context).data(manga).target(CoverViewTarget(cover_thumbnail)) val request = LoadRequest.Builder(view.context).data(manga)
.transformations(RoundedCornersTransformation(2f, 2f, 2f, 2f)).build() .target(CoverViewTarget(cover_thumbnail)).build()
Coil.imageLoader(view.context).execute(request) Coil.imageLoader(view.context).execute(request)
} }
} }

View File

@ -10,23 +10,31 @@ import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.view.gone import eu.kanade.tachiyomi.util.view.gone
import eu.kanade.tachiyomi.util.view.visible import eu.kanade.tachiyomi.util.view.visible
class CoverViewTarget(view: ImageView, val progress: View? = null) : ImageViewTarget(view) { class CoverViewTarget(
view: ImageView,
val progress: View? = null,
val scaleType: ImageView.ScaleType = ImageView.ScaleType.CENTER_CROP
) : ImageViewTarget(view) {
override fun onError(error: Drawable?) { override fun onError(error: Drawable?) {
progress?.gone() progress?.gone()
view.scaleType = ImageView.ScaleType.CENTER view.scaleType = ImageView.ScaleType.CENTER
val vector = VectorDrawableCompat.create(view.context.resources, R.drawable.ic_broken_image_grey_24dp, null) val vector = VectorDrawableCompat.create(
view.context.resources, R.drawable.ic_broken_image_grey_24dp, null
)
vector?.setTint(view.context.getResourceColor(android.R.attr.textColorSecondary)) vector?.setTint(view.context.getResourceColor(android.R.attr.textColorSecondary))
view.setImageDrawable(vector) view.setImageDrawable(vector)
} }
override fun onStart(placeholder: Drawable?) { override fun onStart(placeholder: Drawable?) {
progress?.visible() progress?.visible()
view.scaleType = scaleType
super.onStart(placeholder) super.onStart(placeholder)
} }
override fun onSuccess(result: Drawable) { override fun onSuccess(result: Drawable) {
progress?.gone() progress?.gone()
view.scaleType = scaleType
super.onSuccess(result) super.onSuccess(result)
} }
} }