diff --git a/coil-base/src/main/java/coil/bitmap/BitmapPool.kt b/coil-base/src/main/java/coil/bitmap/BitmapPool.kt index 25f517c7d8..4a2234bb39 100644 --- a/coil-base/src/main/java/coil/bitmap/BitmapPool.kt +++ b/coil-base/src/main/java/coil/bitmap/BitmapPool.kt @@ -18,7 +18,9 @@ interface BitmapPool { */ @JvmStatic @JvmName("create") - operator fun invoke(maxSize: Int): BitmapPool = RealBitmapPool(maxSize) + operator fun invoke(maxSize: Int): BitmapPool { + return if (maxSize == 0) EmptyBitmapPool() else RealBitmapPool(maxSize) + } } /** diff --git a/coil-base/src/main/java/coil/bitmap/EmptyBitmapPool.kt b/coil-base/src/main/java/coil/bitmap/EmptyBitmapPool.kt new file mode 100644 index 0000000000..ef0b0bd911 --- /dev/null +++ b/coil-base/src/main/java/coil/bitmap/EmptyBitmapPool.kt @@ -0,0 +1,35 @@ +package coil.bitmap + +import android.graphics.Bitmap +import androidx.core.graphics.createBitmap +import coil.util.isHardware + +/** A lock-free [BitmapPool] implementation that recycles any [Bitmap]s that are added to it. */ +internal class EmptyBitmapPool : BitmapPool { + + override fun put(bitmap: Bitmap) { + bitmap.recycle() + } + + override fun get(width: Int, height: Int, config: Bitmap.Config) = getDirty(width, height, config) + + override fun getOrNull(width: Int, height: Int, config: Bitmap.Config) = getDirtyOrNull(width, height, config) + + override fun getDirty(width: Int, height: Int, config: Bitmap.Config): Bitmap { + assertNotHardware(config) + return createBitmap(width, height, config) + } + + override fun getDirtyOrNull(width: Int, height: Int, config: Bitmap.Config): Bitmap? { + assertNotHardware(config) + return null + } + + override fun trimMemory(level: Int) {} + + override fun clear() {} + + private fun assertNotHardware(config: Bitmap.Config) { + require(!config.isHardware) { "Cannot create a mutable hardware bitmap." } + } +} diff --git a/coil-base/src/test/java/coil/bitmap/RealBitmapPoolTest.kt b/coil-base/src/test/java/coil/bitmap/RealBitmapPoolTest.kt index b7b62698e2..b2e0c5307c 100644 --- a/coil-base/src/test/java/coil/bitmap/RealBitmapPoolTest.kt +++ b/coil-base/src/test/java/coil/bitmap/RealBitmapPoolTest.kt @@ -7,13 +7,16 @@ import android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL import android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW import android.content.ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN import android.graphics.Bitmap +import android.os.Build.VERSION.SDK_INT import coil.util.DEFAULT_BITMAP_SIZE import coil.util.createBitmap +import org.junit.Assume.assumeTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertTrue @RunWith(RobolectricTestRunner::class) @@ -55,7 +58,7 @@ class RealBitmapPoolTest { @Test fun `clear memory removes all bitmaps`() { pool.fill(MAX_BITMAPS) - pool.clearMemory() + pool.clear() assertEquals(MAX_BITMAPS, strategy.numRemoves) } @@ -64,7 +67,7 @@ class RealBitmapPoolTest { fun `evicted bitmaps are recycled`() { pool.fill(MAX_BITMAPS) val bitmaps = strategy.bitmaps.toList() - pool.clearMemory() + pool.clear() bitmaps.forEach { assertTrue(it.isRecycled) } } @@ -152,7 +155,32 @@ class RealBitmapPoolTest { assertEquals(1, strategy.numPuts) } - private fun RealBitmapPool.fill(fillCount: Int) { + @Test + fun `real - getting a hardware bitmap throws`() { + assumeTrue(SDK_INT >= 26) + + val bitmap = createBitmap(config = Bitmap.Config.HARDWARE) + + pool.put(bitmap) + + assertFailsWith { pool.get(bitmap.width, bitmap.height, bitmap.config) } + assertFailsWith { pool.getOrNull(bitmap.width, bitmap.height, bitmap.config) } + } + + @Test + fun `empty - getting a hardware bitmap throws`() { + assumeTrue(SDK_INT >= 26) + + val pool = EmptyBitmapPool() + val bitmap = createBitmap(config = Bitmap.Config.HARDWARE) + + pool.put(bitmap) + + assertFailsWith { pool.get(bitmap.width, bitmap.height, bitmap.config) } + assertFailsWith { pool.getOrNull(bitmap.width, bitmap.height, bitmap.config) } + } + + private fun BitmapPool.fill(fillCount: Int) { repeat(fillCount) { put(createBitmap()) } } diff --git a/coil-base/src/test/java/coil/util/TestFunctions.kt b/coil-base/src/test/java/coil/util/TestFunctions.kt index 06bfaca66c..22451b05c5 100644 --- a/coil-base/src/test/java/coil/util/TestFunctions.kt +++ b/coil-base/src/test/java/coil/util/TestFunctions.kt @@ -3,6 +3,7 @@ package coil.util import android.graphics.Bitmap +import android.os.Build.VERSION.SDK_INT import android.os.Looper import androidx.core.graphics.createBitmap import org.robolectric.Shadows @@ -13,7 +14,7 @@ fun createBitmap( width: Int = 100, height: Int = 100, config: Bitmap.Config = Bitmap.Config.ARGB_8888, - isMutable: Boolean = true + isMutable: Boolean = SDK_INT < 26 || config != Bitmap.Config.HARDWARE ): Bitmap { val bitmap = createBitmap(width, height, config) Shadows.shadowOf(bitmap).setMutable(isMutable) diff --git a/coil-gif/api/coil-gif.api b/coil-gif/api/coil-gif.api index cd10d0742e..23932f706d 100644 --- a/coil-gif/api/coil-gif.api +++ b/coil-gif/api/coil-gif.api @@ -23,6 +23,7 @@ public final class coil/decode/ImageDecoderDecoder$Companion { public final class coil/drawable/MovieDrawable : android/graphics/drawable/Drawable, androidx/vectordrawable/graphics/drawable/Animatable2Compat { public static final field Companion Lcoil/drawable/MovieDrawable$Companion; public static final field REPEAT_INFINITE I + public fun (Landroid/graphics/Movie;)V public fun (Landroid/graphics/Movie;Lcoil/bitmap/BitmapPool;)V public fun (Landroid/graphics/Movie;Lcoil/bitmap/BitmapPool;Landroid/graphics/Bitmap$Config;)V public fun (Landroid/graphics/Movie;Lcoil/bitmap/BitmapPool;Landroid/graphics/Bitmap$Config;Lcoil/size/Scale;)V diff --git a/coil-gif/src/main/java/coil/drawable/MovieDrawable.kt b/coil-gif/src/main/java/coil/drawable/MovieDrawable.kt index b0158c4ad1..f13a89503d 100644 --- a/coil-gif/src/main/java/coil/drawable/MovieDrawable.kt +++ b/coil-gif/src/main/java/coil/drawable/MovieDrawable.kt @@ -29,7 +29,7 @@ import coil.size.Scale */ class MovieDrawable @JvmOverloads constructor( private val movie: Movie, - private val pool: BitmapPool, + private val pool: BitmapPool = BitmapPool(0), val config: Bitmap.Config = Bitmap.Config.ARGB_8888, val scale: Scale = Scale.FIT ) : Drawable(), Animatable2Compat {