浏览代码

Coil 2.x upgrade (#6725)

* Migrate to Coil 2

* Adapt to use coil disk cache

* Update to alpha 7

* Update to alpha 8

* Update to rc01
Ivan Iskandar 3 年之前
父节点
当前提交
10eef282fa
共有 24 个文件被更改,包括 287 次插入205 次删除
  1. 33 8
      app/src/main/java/eu/kanade/tachiyomi/App.kt
  2. 1 1
      app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt
  3. 0 25
      app/src/main/java/eu/kanade/tachiyomi/data/coil/ByteBufferFetcher.kt
  4. 152 87
      app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverFetcher.kt
  5. 11 0
      app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverKeyer.kt
  6. 32 24
      app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt
  7. 0 3
      app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt
  8. 2 2
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionHolder.kt
  9. 4 4
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaHolder.kt
  10. 4 4
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceComfortableGridHolder.kt
  11. 4 4
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceCompactGridHolder.kt
  12. 4 4
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceListHolder.kt
  13. 4 4
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchCardHolder.kt
  14. 4 4
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryComfortableGridHolder.kt
  15. 5 5
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCompactGridHolder.kt
  16. 4 4
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt
  17. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt
  18. 3 3
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt
  19. 4 4
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchHolder.kt
  20. 2 2
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt
  21. 4 4
      app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryHolder.kt
  22. 4 4
      app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesHolder.kt
  23. 4 3
      app/src/main/java/eu/kanade/tachiyomi/util/view/ImageViewExtensions.kt
  24. 1 1
      gradle/libs.versions.toml

+ 33 - 8
app/src/main/java/eu/kanade/tachiyomi/App.kt

@@ -20,9 +20,10 @@ import coil.ImageLoader
 import coil.ImageLoaderFactory
 import coil.decode.GifDecoder
 import coil.decode.ImageDecoderDecoder
+import coil.disk.DiskCache
 import coil.util.DebugLogger
-import eu.kanade.tachiyomi.data.coil.ByteBufferFetcher
 import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
+import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer
 import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
 import eu.kanade.tachiyomi.data.notification.Notifications
 import eu.kanade.tachiyomi.data.preference.PreferenceValues
@@ -121,17 +122,20 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
 
     override fun newImageLoader(): ImageLoader {
         return ImageLoader.Builder(this).apply {
-            componentRegistry {
+            val callFactoryInit = { Injekt.get<NetworkHelper>().client }
+            val diskCacheInit = { CoilDiskCache.get(this@App) }
+            components {
                 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
-                    add(ImageDecoderDecoder(this@App))
+                    add(ImageDecoderDecoder.Factory())
                 } else {
-                    add(GifDecoder())
+                    add(GifDecoder.Factory())
                 }
-                add(TachiyomiImageDecoder([email protected]))
-                add(ByteBufferFetcher())
-                add(MangaCoverFetcher())
+                add(TachiyomiImageDecoder.Factory())
+                add(MangaCoverFetcher.Factory(lazy(callFactoryInit), lazy(diskCacheInit)))
+                add(MangaCoverKeyer())
             }
-            okHttpClient(Injekt.get<NetworkHelper>().coilClient)
+            callFactory(callFactoryInit)
+            diskCache(diskCacheInit)
             crossfade((300 * [email protected]).toInt())
             allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
             if (preferences.verboseLogging()) logger(DebugLogger())
@@ -190,3 +194,24 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
 }
 
 private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNITO_MODE"
+
+/**
+ * Direct copy of Coil's internal SingletonDiskCache so that [MangaCoverFetcher] can access it.
+ */
+internal object CoilDiskCache {
+
+    private const val FOLDER_NAME = "image_cache"
+    private var instance: DiskCache? = null
+
+    @Synchronized
+    fun get(context: Context): DiskCache {
+        return instance ?: run {
+            val safeCacheDir = context.cacheDir.apply { mkdirs() }
+            // Create the singleton disk cache instance.
+            DiskCache.Builder()
+                .directory(safeCacheDir.resolve(FOLDER_NAME))
+                .build()
+                .also { instance = it }
+        }
+    }
+}

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt

@@ -104,7 +104,7 @@ class CoverCache(private val context: Context) {
      * Clear coil's memory cache.
      */
     fun clearMemoryCache() {
-        context.imageLoader.memoryCache.clear()
+        context.imageLoader.memoryCache?.clear()
     }
 
     private fun getCacheDir(dir: String): File {

+ 0 - 25
app/src/main/java/eu/kanade/tachiyomi/data/coil/ByteBufferFetcher.kt

@@ -1,25 +0,0 @@
-package eu.kanade.tachiyomi.data.coil
-
-import coil.bitmap.BitmapPool
-import coil.decode.DataSource
-import coil.decode.Options
-import coil.fetch.FetchResult
-import coil.fetch.Fetcher
-import coil.fetch.SourceResult
-import coil.size.Size
-import okio.buffer
-import okio.source
-import java.io.ByteArrayInputStream
-import java.nio.ByteBuffer
-
-class ByteBufferFetcher : Fetcher<ByteBuffer> {
-    override suspend fun fetch(pool: BitmapPool, data: ByteBuffer, size: Size, options: Options): FetchResult {
-        return SourceResult(
-            source = ByteArrayInputStream(data.array()).source().buffer(),
-            mimeType = null,
-            dataSource = DataSource.MEMORY
-        )
-    }
-
-    override fun key(data: ByteBuffer): String? = null
-}

+ 152 - 87
app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverFetcher.kt

@@ -1,18 +1,18 @@
 package eu.kanade.tachiyomi.data.coil
 
-import coil.bitmap.BitmapPool
+import coil.ImageLoader
 import coil.decode.DataSource
-import coil.decode.Options
+import coil.decode.ImageSource
+import coil.disk.DiskCache
 import coil.fetch.FetchResult
 import coil.fetch.Fetcher
 import coil.fetch.SourceResult
 import coil.network.HttpException
-import coil.request.get
-import coil.size.Size
+import coil.request.Options
+import coil.request.Parameters
 import eu.kanade.tachiyomi.data.cache.CoverCache
 import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher.Companion.USE_CUSTOM_COVER
 import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.network.NetworkHelper
 import eu.kanade.tachiyomi.network.await
 import eu.kanade.tachiyomi.source.SourceManager
 import eu.kanade.tachiyomi.source.online.HttpSource
@@ -20,130 +20,181 @@ import okhttp3.CacheControl
 import okhttp3.Call
 import okhttp3.Request
 import okhttp3.Response
-import okhttp3.ResponseBody
-import okio.buffer
-import okio.sink
-import okio.source
-import uy.kohesive.injekt.Injekt
-import uy.kohesive.injekt.api.get
+import okhttp3.internal.closeQuietly
+import okio.Path.Companion.toOkioPath
 import uy.kohesive.injekt.injectLazy
 import java.io.File
+import java.net.HttpURLConnection
 
 /**
- * Coil component that fetches [Manga] cover while using the cached file in disk when available.
+ * A [Fetcher] that fetches cover image for [Manga] object.
+ *
+ * It uses [Manga.thumbnail_url] if custom cover is not set by the user.
+ * Disk caching for library items is handled by [CoverCache], otherwise
+ * handled by Coil's [DiskCache].
  *
  * Available request parameter:
  * - [USE_CUSTOM_COVER]: Use custom cover if set by user, default is true
  */
-class MangaCoverFetcher : Fetcher<Manga> {
-    private val coverCache: CoverCache by injectLazy()
-    private val sourceManager: SourceManager by injectLazy()
-    private val defaultClient = Injekt.get<NetworkHelper>().coilClient
-
-    override fun key(data: Manga): String? {
-        if (data.thumbnail_url.isNullOrBlank()) return null
-        return data.thumbnail_url!!
-    }
-
-    override suspend fun fetch(pool: BitmapPool, data: Manga, size: Size, options: Options): FetchResult {
+class MangaCoverFetcher(
+    private val manga: Manga,
+    private val sourceLazy: Lazy<HttpSource?>,
+    private val options: Options,
+    private val coverCache: CoverCache,
+    private val callFactoryLazy: Lazy<Call.Factory>,
+    private val diskCacheLazy: Lazy<DiskCache>
+) : Fetcher {
+
+    // For non-custom cover
+    private val diskCacheKey: String? by lazy { MangaCoverKeyer().key(manga, options) }
+    private lateinit var url: String
+
+    override suspend fun fetch(): FetchResult {
         // Use custom cover if exists
-        val useCustomCover = options.parameters[USE_CUSTOM_COVER] as? Boolean ?: true
-        val customCoverFile = coverCache.getCustomCoverFile(data)
+        val useCustomCover = options.parameters.value(USE_CUSTOM_COVER) ?: true
+        val customCoverFile = coverCache.getCustomCoverFile(manga)
         if (useCustomCover && customCoverFile.exists()) {
             return fileLoader(customCoverFile)
         }
 
-        val cover = data.thumbnail_url
-        return when (getResourceType(cover)) {
-            Type.URL -> httpLoader(data, options)
-            Type.File -> fileLoader(data)
+        // diskCacheKey is thumbnail_url
+        url = diskCacheKey ?: error("No cover specified")
+        return when (getResourceType(url)) {
+            Type.URL -> httpLoader()
+            Type.File -> fileLoader(File(url.substringAfter("file://")))
             null -> error("Invalid image")
         }
     }
 
-    private suspend fun httpLoader(manga: Manga, options: Options): FetchResult {
+    private fun fileLoader(file: File): FetchResult {
+        return SourceResult(
+            source = ImageSource(file = file.toOkioPath(), diskCacheKey = diskCacheKey),
+            mimeType = "image/*",
+            dataSource = DataSource.DISK
+        )
+    }
+
+    private suspend fun httpLoader(): FetchResult {
         // Only cache separately if it's a library item
         val coverCacheFile = if (manga.favorite) {
             coverCache.getCoverFile(manga) ?: error("No cover specified")
         } else {
             null
         }
-
         if (coverCacheFile?.exists() == true && options.diskCachePolicy.readEnabled) {
             return fileLoader(coverCacheFile)
         }
 
-        val (response, body) = awaitGetCall(manga, options)
-        if (!response.isSuccessful) {
-            body.close()
-            throw HttpException(response)
-        }
+        var snapshot = readFromDiskCache()
+        try {
+            // Fetch from disk cache
+            if (snapshot != null) {
+                return SourceResult(
+                    source = snapshot.toImageSource(),
+                    mimeType = "image/*",
+                    dataSource = DataSource.DISK
+                )
+            }
 
-        if (coverCacheFile != null && options.diskCachePolicy.writeEnabled) {
-            @Suppress("BlockingMethodInNonBlockingContext")
-            response.peekBody(Long.MAX_VALUE).source().use { input ->
-                coverCacheFile.parentFile?.mkdirs()
-                if (coverCacheFile.exists()) {
-                    coverCacheFile.delete()
-                }
-                coverCacheFile.sink().buffer().use { output ->
-                    output.writeAll(input)
+            // Fetch from network
+            val response = executeNetworkRequest()
+            val responseBody = checkNotNull(response.body) { "Null response source" }
+            try {
+                snapshot = writeToDiskCache(snapshot, response)
+                // Read from disk cache
+                if (snapshot != null) {
+                    return SourceResult(
+                        source = snapshot.toImageSource(),
+                        mimeType = "image/*",
+                        dataSource = DataSource.NETWORK
+                    )
                 }
+
+                // Read from response if cache is unused or unusable
+                return SourceResult(
+                    source = ImageSource(source = responseBody.source(), context = options.context),
+                    mimeType = "image/*",
+                    dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK
+                )
+            } catch (e: Exception) {
+                responseBody.closeQuietly()
+                throw e
+            } finally {
+                response.close()
             }
+        } catch (e: Exception) {
+            snapshot?.closeQuietly()
+            throw e
         }
-
-        return SourceResult(
-            source = body.source(),
-            mimeType = "image/*",
-            dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK
-        )
     }
 
-    private suspend fun awaitGetCall(manga: Manga, options: Options): Pair<Response, ResponseBody> {
-        val call = getCall(manga, options)
-        val response = call.await()
-        return response to checkNotNull(response.body) { "Null response source" }
+    private suspend fun executeNetworkRequest(): Response {
+        val client = sourceLazy.value?.client ?: callFactoryLazy.value
+        val response = client.newCall(newRequest()).await()
+        if (!response.isSuccessful && response.code != HttpURLConnection.HTTP_NOT_MODIFIED) {
+            response.body?.closeQuietly()
+            throw HttpException(response)
+        }
+        return response
     }
 
-    private fun getCall(manga: Manga, options: Options): Call {
-        val source = sourceManager.get(manga.source) as? HttpSource
-        val request = Request.Builder().url(manga.thumbnail_url!!).also {
-            if (source != null) {
-                it.headers(source.headers)
+    private fun newRequest(): Request {
+        val request = Request.Builder()
+            .url(url)
+            .headers(options.headers)
+            // Support attaching custom data to the network request.
+            .tag(Parameters::class.java, options.parameters)
+
+        val diskRead = options.diskCachePolicy.readEnabled
+        val networkRead = options.networkCachePolicy.readEnabled
+        when {
+            !networkRead && diskRead -> {
+                request.cacheControl(CacheControl.FORCE_CACHE)
             }
-
-            val networkRead = options.networkCachePolicy.readEnabled
-            val diskRead = options.diskCachePolicy.readEnabled
-            when {
-                !networkRead && diskRead -> {
-                    it.cacheControl(CacheControl.FORCE_CACHE)
-                }
-                networkRead && !diskRead -> if (options.diskCachePolicy.writeEnabled) {
-                    it.cacheControl(CacheControl.FORCE_NETWORK)
-                } else {
-                    it.cacheControl(CACHE_CONTROL_FORCE_NETWORK_NO_CACHE)
-                }
-                !networkRead && !diskRead -> {
-                    // This causes the request to fail with a 504 Unsatisfiable Request.
-                    it.cacheControl(CACHE_CONTROL_NO_NETWORK_NO_CACHE)
-                }
+            networkRead && !diskRead -> if (options.diskCachePolicy.writeEnabled) {
+                request.cacheControl(CacheControl.FORCE_NETWORK)
+            } else {
+                request.cacheControl(CACHE_CONTROL_FORCE_NETWORK_NO_CACHE)
             }
-        }.build()
+            !networkRead && !diskRead -> {
+                // This causes the request to fail with a 504 Unsatisfiable Request.
+                request.cacheControl(CACHE_CONTROL_NO_NETWORK_NO_CACHE)
+            }
+        }
 
-        val client = source?.client?.newBuilder()?.cache(defaultClient.cache)?.build() ?: defaultClient
-        return client.newCall(request)
+        return request.build()
     }
 
-    private fun fileLoader(manga: Manga): FetchResult {
-        return fileLoader(File(manga.thumbnail_url!!.substringAfter("file://")))
+    private fun readFromDiskCache(): DiskCache.Snapshot? {
+        return if (options.diskCachePolicy.readEnabled) diskCacheLazy.value[diskCacheKey!!] else null
     }
 
-    private fun fileLoader(file: File): FetchResult {
-        return SourceResult(
-            source = file.source().buffer(),
-            mimeType = "image/*",
-            dataSource = DataSource.DISK
-        )
+    private fun writeToDiskCache(snapshot: DiskCache.Snapshot?, response: Response): DiskCache.Snapshot? {
+        if (!options.diskCachePolicy.writeEnabled) {
+            snapshot?.closeQuietly()
+            return null
+        }
+        val editor = if (snapshot != null) {
+            snapshot.closeAndEdit()
+        } else {
+            diskCacheLazy.value.edit(diskCacheKey!!)
+        } ?: return null
+        try {
+            diskCacheLazy.value.fileSystem.write(editor.data) {
+                response.body!!.source().readAll(this)
+            }
+            return editor.commitAndGet()
+        } catch (e: Exception) {
+            try {
+                editor.abort()
+            } catch (ignored: Exception) {
+            }
+            throw e
+        }
+    }
+
+    private fun DiskCache.Snapshot.toImageSource(): ImageSource {
+        return ImageSource(file = data, diskCacheKey = diskCacheKey, closeable = this)
     }
 
     private fun getResourceType(cover: String?): Type? {
@@ -159,6 +210,20 @@ class MangaCoverFetcher : Fetcher<Manga> {
         File, URL
     }
 
+    class Factory(
+        private val callFactoryLazy: Lazy<Call.Factory>,
+        private val diskCacheLazy: Lazy<DiskCache>
+    ) : Fetcher.Factory<Manga> {
+
+        private val coverCache: CoverCache by injectLazy()
+        private val sourceManager: SourceManager by injectLazy()
+
+        override fun create(data: Manga, options: Options, imageLoader: ImageLoader): Fetcher {
+            val source = lazy { sourceManager.get(data.source) as? HttpSource }
+            return MangaCoverFetcher(data, source, options, coverCache, callFactoryLazy, diskCacheLazy)
+        }
+    }
+
     companion object {
         const val USE_CUSTOM_COVER = "use_custom_cover"
 

+ 11 - 0
app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverKeyer.kt

@@ -0,0 +1,11 @@
+package eu.kanade.tachiyomi.data.coil
+
+import coil.key.Keyer
+import coil.request.Options
+import eu.kanade.tachiyomi.data.database.models.Manga
+
+class MangaCoverKeyer : Keyer<Manga> {
+    override fun key(data: Manga, options: Options): String? {
+        return data.thumbnail_url?.takeIf { it.isNotBlank() }
+    }
+}

+ 32 - 24
app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt

@@ -1,13 +1,14 @@
 package eu.kanade.tachiyomi.data.coil
 
-import android.content.res.Resources
 import android.os.Build
 import androidx.core.graphics.drawable.toDrawable
-import coil.bitmap.BitmapPool
+import coil.ImageLoader
 import coil.decode.DecodeResult
 import coil.decode.Decoder
-import coil.decode.Options
-import coil.size.Size
+import coil.decode.ImageDecoderDecoder
+import coil.decode.ImageSource
+import coil.fetch.SourceResult
+import coil.request.Options
 import eu.kanade.tachiyomi.util.system.ImageUtil
 import okio.BufferedSource
 import tachiyomi.decoder.ImageDecoder
@@ -15,26 +16,10 @@ import tachiyomi.decoder.ImageDecoder
 /**
  * A [Decoder] that uses built-in [ImageDecoder] to decode images that is not supported by the system.
  */
-class TachiyomiImageDecoder(private val resources: Resources) : Decoder {
+class TachiyomiImageDecoder(private val resources: ImageSource, private val options: Options) : Decoder {
 
-    override fun handles(source: BufferedSource, mimeType: String?): Boolean {
-        val type = source.peek().inputStream().use {
-            ImageUtil.findImageType(it)
-        }
-        return when (type) {
-            ImageUtil.ImageType.AVIF, ImageUtil.ImageType.JXL -> true
-            ImageUtil.ImageType.HEIF -> Build.VERSION.SDK_INT < Build.VERSION_CODES.O
-            else -> false
-        }
-    }
-
-    override suspend fun decode(
-        pool: BitmapPool,
-        source: BufferedSource,
-        size: Size,
-        options: Options
-    ): DecodeResult {
-        val decoder = source.use {
+    override suspend fun decode(): DecodeResult {
+        val decoder = resources.sourceOrNull()?.use {
             ImageDecoder.newInstance(it.inputStream())
         }
 
@@ -46,8 +31,31 @@ class TachiyomiImageDecoder(private val resources: Resources) : Decoder {
         check(bitmap != null) { "Failed to decode image." }
 
         return DecodeResult(
-            drawable = bitmap.toDrawable(resources),
+            drawable = bitmap.toDrawable(options.context.resources),
             isSampled = false
         )
     }
+
+    class Factory : Decoder.Factory {
+
+        override fun create(result: SourceResult, options: Options, imageLoader: ImageLoader): Decoder? {
+            if (!isApplicable(result.source.source())) return null
+            return TachiyomiImageDecoder(result.source, options)
+        }
+
+        private fun isApplicable(source: BufferedSource): Boolean {
+            val type = source.peek().inputStream().use {
+                ImageUtil.findImageType(it)
+            }
+            return when (type) {
+                ImageUtil.ImageType.AVIF, ImageUtil.ImageType.JXL -> true
+                ImageUtil.ImageType.HEIF -> Build.VERSION.SDK_INT < Build.VERSION_CODES.O
+                else -> false
+            }
+        }
+
+        override fun equals(other: Any?) = other is ImageDecoderDecoder.Factory
+
+        override fun hashCode() = javaClass.hashCode()
+    }
 }

+ 0 - 3
app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt

@@ -1,7 +1,6 @@
 package eu.kanade.tachiyomi.network
 
 import android.content.Context
-import coil.util.CoilUtils
 import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
 import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
@@ -49,8 +48,6 @@ class NetworkHelper(context: Context) {
 
     val client by lazy { baseClientBuilder.cache(Cache(cacheDir, cacheSize)).build() }
 
-    val coilClient by lazy { baseClientBuilder.cache(CoilUtils.createDefaultCache(context)).build() }
-
     val cloudflareClient by lazy {
         client.newBuilder()
             .addInterceptor(CloudflareInterceptor(context))

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionHolder.kt

@@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.browse.extension
 
 import android.view.View
 import androidx.core.view.isVisible
-import coil.clear
+import coil.dispose
 import coil.load
 import eu.davidea.viewholders.FlexibleViewHolder
 import eu.kanade.tachiyomi.R
@@ -39,7 +39,7 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
             else -> ""
         }.uppercase()
 
-        binding.icon.clear()
+        binding.icon.dispose()
         if (extension is Extension.Available) {
             binding.icon.load(extension.iconUrl)
         } else if (extension is Extension.Installed) {

+ 4 - 4
app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaHolder.kt

@@ -1,8 +1,8 @@
 package eu.kanade.tachiyomi.ui.browse.migration.manga
 
 import android.view.View
-import coil.clear
-import coil.loadAny
+import coil.dispose
+import coil.load
 import eu.davidea.viewholders.FlexibleViewHolder
 import eu.kanade.tachiyomi.databinding.SourceListItemBinding
 
@@ -23,7 +23,7 @@ class MigrationMangaHolder(
         binding.title.text = item.manga.title
 
         // Update the cover
-        binding.thumbnail.clear()
-        binding.thumbnail.loadAny(item.manga)
+        binding.thumbnail.dispose()
+        binding.thumbnail.load(item.manga)
     }
 }

+ 4 - 4
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceComfortableGridHolder.kt

@@ -1,7 +1,7 @@
 package eu.kanade.tachiyomi.ui.browse.source.browse
 
 import androidx.core.view.isVisible
-import coil.clear
+import coil.dispose
 import coil.imageLoader
 import coil.request.ImageRequest
 import coil.transition.CrossfadeTransition
@@ -48,10 +48,10 @@ class SourceComfortableGridHolder(
     }
 
     override fun setImage(manga: Manga) {
-        binding.thumbnail.clear()
+        binding.thumbnail.dispose()
         if (!manga.thumbnail_url.isNullOrEmpty()) {
-            val crossfadeDuration = binding.root.context.imageLoader.defaults.transition.let {
-                if (it is CrossfadeTransition) it.durationMillis else 0
+            val crossfadeDuration = binding.root.context.imageLoader.defaults.transitionFactory.let {
+                if (it is CrossfadeTransition.Factory) it.durationMillis else 0
             }
             val request = ImageRequest.Builder(binding.root.context)
                 .data(manga)

+ 4 - 4
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceCompactGridHolder.kt

@@ -1,7 +1,7 @@
 package eu.kanade.tachiyomi.ui.browse.source.browse
 
 import androidx.core.view.isVisible
-import coil.clear
+import coil.dispose
 import coil.imageLoader
 import coil.request.ImageRequest
 import coil.transition.CrossfadeTransition
@@ -48,10 +48,10 @@ class SourceCompactGridHolder(
     }
 
     override fun setImage(manga: Manga) {
-        binding.thumbnail.clear()
+        binding.thumbnail.dispose()
         if (!manga.thumbnail_url.isNullOrEmpty()) {
-            val crossfadeDuration = binding.root.context.imageLoader.defaults.transition.let {
-                if (it is CrossfadeTransition) it.durationMillis else 0
+            val crossfadeDuration = binding.root.context.imageLoader.defaults.transitionFactory.let {
+                if (it is CrossfadeTransition.Factory) it.durationMillis else 0
             }
             val request = ImageRequest.Builder(binding.root.context)
                 .data(manga)

+ 4 - 4
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceListHolder.kt

@@ -2,8 +2,8 @@ package eu.kanade.tachiyomi.ui.browse.source.browse
 
 import android.view.View
 import androidx.core.view.isVisible
-import coil.clear
-import coil.loadAny
+import coil.dispose
+import coil.load
 import eu.davidea.flexibleadapter.FlexibleAdapter
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
@@ -50,9 +50,9 @@ class SourceListHolder(private val view: View, adapter: FlexibleAdapter<*>) :
     }
 
     override fun setImage(manga: Manga) {
-        binding.thumbnail.clear()
+        binding.thumbnail.dispose()
         if (!manga.thumbnail_url.isNullOrEmpty()) {
-            binding.thumbnail.loadAny(manga) {
+            binding.thumbnail.load(manga) {
                 setParameter(MangaCoverFetcher.USE_CUSTOM_COVER, false)
             }
         }

+ 4 - 4
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchCardHolder.kt

@@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.browse.source.globalsearch
 
 import android.view.View
 import androidx.core.view.isVisible
-import coil.clear
+import coil.dispose
 import coil.imageLoader
 import coil.request.ImageRequest
 import coil.transition.CrossfadeTransition
@@ -53,10 +53,10 @@ class GlobalSearchCardHolder(view: View, adapter: GlobalSearchCardAdapter) :
     }
 
     fun setImage(manga: Manga) {
-        binding.cover.clear()
+        binding.cover.dispose()
         if (!manga.thumbnail_url.isNullOrEmpty()) {
-            val crossfadeDuration = itemView.context.imageLoader.defaults.transition.let {
-                if (it is CrossfadeTransition) it.durationMillis else 0
+            val crossfadeDuration = itemView.context.imageLoader.defaults.transitionFactory.let {
+                if (it is CrossfadeTransition.Factory) it.durationMillis else 0
             }
             val request = ImageRequest.Builder(itemView.context)
                 .data(manga)

+ 4 - 4
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryComfortableGridHolder.kt

@@ -2,11 +2,11 @@ package eu.kanade.tachiyomi.ui.library
 
 import androidx.core.view.isVisible
 import androidx.recyclerview.widget.RecyclerView
-import coil.clear
+import coil.dispose
 import eu.davidea.flexibleadapter.FlexibleAdapter
 import eu.davidea.flexibleadapter.items.IFlexible
 import eu.kanade.tachiyomi.databinding.SourceComfortableGridItemBinding
-import eu.kanade.tachiyomi.util.view.loadAnyAutoPause
+import eu.kanade.tachiyomi.util.view.loadAutoPause
 
 /**
  * Class used to hold the displayed data of a manga in the library, like the cover or the title.
@@ -55,7 +55,7 @@ class LibraryComfortableGridHolder(
         binding.badges.localText.isVisible = item.isLocal
 
         // Update the cover.
-        binding.thumbnail.clear()
-        binding.thumbnail.loadAnyAutoPause(item.manga)
+        binding.thumbnail.dispose()
+        binding.thumbnail.loadAutoPause(item.manga)
     }
 }

+ 5 - 5
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCompactGridHolder.kt

@@ -1,10 +1,10 @@
 package eu.kanade.tachiyomi.ui.library
 
 import androidx.core.view.isVisible
-import coil.clear
+import coil.dispose
 import eu.davidea.flexibleadapter.FlexibleAdapter
 import eu.kanade.tachiyomi.databinding.SourceCompactGridItemBinding
-import eu.kanade.tachiyomi.util.view.loadAnyAutoPause
+import eu.kanade.tachiyomi.util.view.loadAutoPause
 
 /**
  * Class used to hold the displayed data of a manga in the library, like the cover or the title.
@@ -54,11 +54,11 @@ class LibraryCompactGridHolder(
         binding.badges.localText.isVisible = item.isLocal
 
         // Update the cover.
-        binding.thumbnail.clear()
+        binding.thumbnail.dispose()
         if (coverOnly) {
             // Cover only mode: Hides title text unless thumbnail is unavailable
             if (!item.manga.thumbnail_url.isNullOrEmpty()) {
-                binding.thumbnail.loadAnyAutoPause(item.manga)
+                binding.thumbnail.loadAutoPause(item.manga)
                 binding.title.isVisible = false
             } else {
                 binding.title.text = item.manga.title
@@ -66,7 +66,7 @@ class LibraryCompactGridHolder(
             }
             binding.thumbnail.foreground = null
         } else {
-            binding.thumbnail.loadAnyAutoPause(item.manga)
+            binding.thumbnail.loadAutoPause(item.manga)
         }
     }
 }

+ 4 - 4
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt

@@ -2,8 +2,8 @@ package eu.kanade.tachiyomi.ui.library
 
 import android.view.View
 import androidx.core.view.isVisible
-import coil.clear
-import coil.loadAny
+import coil.dispose
+import coil.load
 import eu.davidea.flexibleadapter.FlexibleAdapter
 import eu.kanade.tachiyomi.databinding.SourceListItemBinding
 
@@ -61,7 +61,7 @@ class LibraryListHolder(
         }
 
         // Update the cover
-        binding.thumbnail.clear()
-        binding.thumbnail.loadAny(item.manga)
+        binding.thumbnail.dispose()
+        binding.thumbnail.load(item.manga)
     }
 }

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt

@@ -334,7 +334,7 @@ class MangaPresenter(
      * @return cover as Bitmap or null if there is no thumbnail cached with the memoryCacheKey
      */
     private fun coverBitmapFromImageLoader(context: Context, memoryCacheKey: MemoryCache.Key): Bitmap? {
-        return context.imageLoader.memoryCache[memoryCacheKey]
+        return context.imageLoader.memoryCache?.get(memoryCacheKey)?.bitmap
     }
 
     /**

+ 3 - 3
app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt

@@ -19,7 +19,7 @@ import eu.kanade.tachiyomi.source.online.HttpSource
 import eu.kanade.tachiyomi.ui.base.controller.getMainAppBarHeight
 import eu.kanade.tachiyomi.ui.manga.MangaController
 import eu.kanade.tachiyomi.util.system.copyToClipboard
-import eu.kanade.tachiyomi.util.view.loadAnyAutoPause
+import eu.kanade.tachiyomi.util.view.loadAutoPause
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import reactivecircus.flowbinding.android.view.clicks
@@ -286,8 +286,8 @@ class MangaInfoHeaderAdapter(
             setFavoriteButtonState(manga.favorite)
 
             // Set cover if changed.
-            binding.backdrop.loadAnyAutoPause(manga)
-            binding.mangaCover.loadAnyAutoPause(manga)
+            binding.backdrop.loadAutoPause(manga)
+            binding.mangaCover.loadAutoPause(manga)
 
             // Manga info section
             binding.mangaSummarySection.setTags(manga.getGenres(), controller::performGenreSearch)

+ 4 - 4
app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchHolder.kt

@@ -2,8 +2,8 @@ package eu.kanade.tachiyomi.ui.manga.track
 
 import androidx.core.view.isVisible
 import androidx.recyclerview.widget.RecyclerView
-import coil.clear
-import coil.loadAny
+import coil.dispose
+import coil.load
 import eu.kanade.tachiyomi.data.track.model.TrackSearch
 import eu.kanade.tachiyomi.databinding.TrackSearchItemBinding
 import java.util.Locale
@@ -20,9 +20,9 @@ class TrackSearchHolder(
         }
 
         binding.trackSearchTitle.text = track.title
-        binding.trackSearchCover.clear()
+        binding.trackSearchCover.dispose()
         if (track.cover_url.isNotEmpty()) {
-            binding.trackSearchCover.loadAny(track.cover_url)
+            binding.trackSearchCover.load(track.cover_url)
         }
 
         val hasStatus = track.publishing_status.isNotBlank()

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt

@@ -17,7 +17,7 @@ import androidx.annotation.CallSuper
 import androidx.annotation.StyleRes
 import androidx.appcompat.widget.AppCompatImageView
 import androidx.core.view.isVisible
-import coil.clear
+import coil.dispose
 import coil.imageLoader
 import coil.request.CachePolicy
 import coil.request.ImageRequest
@@ -152,7 +152,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
     fun recycle() = pageView?.let {
         when (it) {
             is SubsamplingScaleImageView -> it.recycle()
-            is AppCompatImageView -> it.clear()
+            is AppCompatImageView -> it.dispose()
         }
         it.isVisible = false
     }

+ 4 - 4
app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryHolder.kt

@@ -1,8 +1,8 @@
 package eu.kanade.tachiyomi.ui.recent.history
 
 import android.view.View
-import coil.clear
-import coil.loadAny
+import coil.dispose
+import coil.load
 import eu.davidea.viewholders.FlexibleViewHolder
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
@@ -65,7 +65,7 @@ class HistoryHolder(
         }
 
         // Set cover
-        binding.cover.clear()
-        binding.cover.loadAny(item.manga)
+        binding.cover.dispose()
+        binding.cover.load(item.manga)
     }
 }

+ 4 - 4
app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesHolder.kt

@@ -2,8 +2,8 @@ package eu.kanade.tachiyomi.ui.recent.updates
 
 import android.view.View
 import androidx.core.view.isVisible
-import coil.clear
-import coil.loadAny
+import coil.dispose
+import coil.load
 import eu.kanade.tachiyomi.databinding.UpdatesItemBinding
 import eu.kanade.tachiyomi.source.LocalSource
 import eu.kanade.tachiyomi.ui.manga.chapter.base.BaseChapterHolder
@@ -58,7 +58,7 @@ class UpdatesHolder(private val view: View, private val adapter: UpdatesAdapter)
         binding.download.setState(item.status, item.progress)
 
         // Set cover
-        binding.mangaCover.clear()
-        binding.mangaCover.loadAny(item.manga)
+        binding.mangaCover.dispose()
+        binding.mangaCover.load(item.manga)
     }
 }

+ 4 - 3
app/src/main/java/eu/kanade/tachiyomi/util/view/ImageViewExtensions.kt

@@ -8,7 +8,7 @@ import androidx.annotation.DrawableRes
 import androidx.appcompat.content.res.AppCompatResources
 import coil.ImageLoader
 import coil.imageLoader
-import coil.loadAny
+import coil.load
 import coil.request.ImageRequest
 import coil.target.ImageViewTarget
 import eu.kanade.tachiyomi.util.system.animatorDurationScale
@@ -33,12 +33,13 @@ fun ImageView.setVectorCompat(@DrawableRes drawable: Int, @AttrRes tint: Int? =
  * and if the image is animated, this will also disable that animation
  * if [Context.animatorDurationScale] is 0
  */
-fun ImageView.loadAnyAutoPause(
+fun ImageView.loadAutoPause(
     data: Any?,
     loader: ImageLoader = context.imageLoader,
     builder: ImageRequest.Builder.() -> Unit = {}
 ) {
-    this.loadAny(data, loader) {
+    // Build the original request so we can add on our success listener
+    load(data, loader) {
         // Build the original request so we can add on our success listener
         val originalBuild = apply(builder).build()
         listener(

+ 1 - 1
gradle/libs.versions.toml

@@ -2,7 +2,7 @@
 aboutlib_version = "8.9.4"
 okhttp_version = "4.9.1"
 nucleus_version = "3.0.0"
-coil_version = "1.4.0"
+coil_version = "2.0.0-rc01"
 conductor_version = "3.1.2"
 flowbinding_version = "1.2.0"
 shizuku_version = "12.1.0"