Merge "Implement common ImageLoader in SystemUI" into udc-dev am: 0c5af73093

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/22198202

Change-Id: Iadc8fa1a6fd5c4a8dd77c6b6b7836f727384c258
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Jernej Virag
2023-03-31 11:03:51 +00:00
committed by Automerger Merge Worker
3 changed files with 839 additions and 0 deletions

View File

@@ -0,0 +1,493 @@
package com.android.systemui.graphics
import android.annotation.AnyThread
import android.annotation.DrawableRes
import android.annotation.Px
import android.annotation.SuppressLint
import android.annotation.WorkerThread
import android.content.Context
import android.content.pm.PackageManager
import android.content.res.Resources
import android.content.res.Resources.NotFoundException
import android.graphics.Bitmap
import android.graphics.ImageDecoder
import android.graphics.ImageDecoder.DecodeException
import android.graphics.drawable.AdaptiveIconDrawable
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.Icon
import android.util.Log
import android.util.Size
import androidx.core.content.res.ResourcesCompat
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import java.io.IOException
import javax.inject.Inject
import kotlin.math.min
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
/**
* Helper class to load images for SystemUI. It allows for memory efficient image loading with size
* restriction and attempts to use hardware bitmaps when sensible.
*/
@SysUISingleton
class ImageLoader
@Inject
constructor(
private val defaultContext: Context,
@Background private val backgroundDispatcher: CoroutineDispatcher
) {
/** Source of the image data. */
sealed interface Source
/**
* Load image from a Resource ID. If the resource is part of another package or if it requires
* tinting, pass in a correct [Context].
*/
data class Res(@DrawableRes val resId: Int, val context: Context?) : Source {
constructor(@DrawableRes resId: Int) : this(resId, null)
}
/** Load image from a Uri. */
data class Uri(val uri: android.net.Uri) : Source {
constructor(uri: String) : this(android.net.Uri.parse(uri))
}
/** Load image from a [File]. */
data class File(val file: java.io.File) : Source {
constructor(path: String) : this(java.io.File(path))
}
/** Load image from an [InputStream]. */
data class InputStream(val inputStream: java.io.InputStream, val context: Context?) : Source {
constructor(inputStream: java.io.InputStream) : this(inputStream, null)
}
/**
* Loads passed [Source] on a background thread and returns the [Bitmap].
*
* Maximum height and width can be passed as optional parameters - the image decoder will make
* sure to keep the decoded drawable size within those passed constraints while keeping aspect
* ratio.
*
* @param maxWidth Maximum width of the returned drawable (if able). 0 means no restriction. Set
* to [DEFAULT_MAX_SAFE_BITMAP_SIZE_PX] by default.
* @param maxHeight Maximum height of the returned drawable (if able). 0 means no restriction.
* Set to [DEFAULT_MAX_SAFE_BITMAP_SIZE_PX] by default.
* @param allocator Allocator to use for the loaded drawable - one of [ImageDecoder] allocator
* ints. Use [ImageDecoder.ALLOCATOR_SOFTWARE] to force software bitmap.
* @return loaded [Bitmap] or `null` if loading failed.
*/
@AnyThread
suspend fun loadBitmap(
source: Source,
@Px maxWidth: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX,
@Px maxHeight: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX,
allocator: Int = ImageDecoder.ALLOCATOR_DEFAULT
): Bitmap? =
withContext(backgroundDispatcher) { loadBitmapSync(source, maxWidth, maxHeight, allocator) }
/**
* Loads passed [Source] synchronously and returns the [Bitmap].
*
* Maximum height and width can be passed as optional parameters - the image decoder will make
* sure to keep the decoded drawable size within those passed constraints while keeping aspect
* ratio.
*
* @param maxWidth Maximum width of the returned drawable (if able). 0 means no restriction. Set
* to [DEFAULT_MAX_SAFE_BITMAP_SIZE_PX] by default.
* @param maxHeight Maximum height of the returned drawable (if able). 0 means no restriction.
* Set to [DEFAULT_MAX_SAFE_BITMAP_SIZE_PX] by default.
* @param allocator Allocator to use for the loaded drawable - one of [ImageDecoder] allocator
* ints. Use [ImageDecoder.ALLOCATOR_SOFTWARE] to force software bitmap.
* @return loaded [Bitmap] or `null` if loading failed.
*/
@WorkerThread
fun loadBitmapSync(
source: Source,
@Px maxWidth: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX,
@Px maxHeight: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX,
allocator: Int = ImageDecoder.ALLOCATOR_DEFAULT
): Bitmap? {
return try {
loadBitmapSync(
toImageDecoderSource(source, defaultContext),
maxWidth,
maxHeight,
allocator
)
} catch (e: NotFoundException) {
Log.w(TAG, "Couldn't load resource $source", e)
null
}
}
/**
* Loads passed [ImageDecoder.Source] synchronously and returns the drawable.
*
* Maximum height and width can be passed as optional parameters - the image decoder will make
* sure to keep the decoded drawable size within those passed constraints (while keeping aspect
* ratio).
*
* @param maxWidth Maximum width of the returned drawable (if able). 0 means no restriction. Set
* to [DEFAULT_MAX_SAFE_BITMAP_SIZE_PX] by default.
* @param maxHeight Maximum height of the returned drawable (if able). 0 means no restriction.
* Set to [DEFAULT_MAX_SAFE_BITMAP_SIZE_PX] by default.
* @param allocator Allocator to use for the loaded drawable - one of [ImageDecoder] allocator
* ints. Use [ImageDecoder.ALLOCATOR_SOFTWARE] to force software bitmap.
* @return loaded [Bitmap] or `null` if loading failed.
*/
@WorkerThread
fun loadBitmapSync(
source: ImageDecoder.Source,
@Px maxWidth: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX,
@Px maxHeight: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX,
allocator: Int = ImageDecoder.ALLOCATOR_DEFAULT
): Bitmap? {
return try {
ImageDecoder.decodeBitmap(source) { decoder, info, _ ->
configureDecoderForMaximumSize(decoder, info.size, maxWidth, maxHeight)
decoder.allocator = allocator
}
} catch (e: IOException) {
Log.w(TAG, "Failed to load source $source", e)
return null
} catch (e: DecodeException) {
Log.w(TAG, "Failed to decode source $source", e)
return null
}
}
/**
* Loads passed [Source] on a background thread and returns the [Drawable].
*
* Maximum height and width can be passed as optional parameters - the image decoder will make
* sure to keep the decoded drawable size within those passed constraints (while keeping aspect
* ratio).
*
* @param maxWidth Maximum width of the returned drawable (if able). 0 means no restriction. Set
* to [DEFAULT_MAX_SAFE_BITMAP_SIZE_PX] by default.
* @param maxHeight Maximum height of the returned drawable (if able). 0 means no restriction.
* Set to [DEFAULT_MAX_SAFE_BITMAP_SIZE_PX] by default.
* @param allocator Allocator to use for the loaded drawable - one of [ImageDecoder] allocator
* ints. Use [ImageDecoder.ALLOCATOR_SOFTWARE] to force software bitmap.
* @return loaded [Drawable] or `null` if loading failed.
*/
@AnyThread
suspend fun loadDrawable(
source: Source,
@Px maxWidth: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX,
@Px maxHeight: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX,
allocator: Int = ImageDecoder.ALLOCATOR_DEFAULT
): Drawable? =
withContext(backgroundDispatcher) {
loadDrawableSync(source, maxWidth, maxHeight, allocator)
}
/**
* Loads passed [Icon] on a background thread and returns the drawable.
*
* Maximum height and width can be passed as optional parameters - the image decoder will make
* sure to keep the decoded drawable size within those passed constraints (while keeping aspect
* ratio).
*
* @param context Alternate context to use for resource loading (for e.g. cross-process use)
* @param maxWidth Maximum width of the returned drawable (if able). 0 means no restriction. Set
* to [DEFAULT_MAX_SAFE_BITMAP_SIZE_PX] by default.
* @param maxHeight Maximum height of the returned drawable (if able). 0 means no restriction.
* Set to [DEFAULT_MAX_SAFE_BITMAP_SIZE_PX] by default.
* @param allocator Allocator to use for the loaded drawable - one of [ImageDecoder] allocator
* ints. Use [ImageDecoder.ALLOCATOR_SOFTWARE] to force software bitmap.
* @return loaded [Drawable] or `null` if loading failed.
*/
@AnyThread
suspend fun loadDrawable(
icon: Icon,
context: Context = defaultContext,
@Px maxWidth: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX,
@Px maxHeight: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX,
allocator: Int = ImageDecoder.ALLOCATOR_DEFAULT
): Drawable? =
withContext(backgroundDispatcher) {
loadDrawableSync(icon, context, maxWidth, maxHeight, allocator)
}
/**
* Loads passed [Source] synchronously and returns the drawable.
*
* Maximum height and width can be passed as optional parameters - the image decoder will make
* sure to keep the decoded drawable size within those passed constraints (while keeping aspect
* ratio).
*
* @param maxWidth Maximum width of the returned drawable (if able). 0 means no restriction. Set
* to [DEFAULT_MAX_SAFE_BITMAP_SIZE_PX] by default.
* @param maxHeight Maximum height of the returned drawable (if able). 0 means no restriction.
* Set to [DEFAULT_MAX_SAFE_BITMAP_SIZE_PX] by default.
* @param allocator Allocator to use for the loaded drawable - one of [ImageDecoder] allocator
* ints. Use [ImageDecoder.ALLOCATOR_SOFTWARE] to force software bitmap.
* @return loaded [Drawable] or `null` if loading failed.
*/
@WorkerThread
@SuppressLint("UseCompatLoadingForDrawables")
fun loadDrawableSync(
source: Source,
@Px maxWidth: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX,
@Px maxHeight: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX,
allocator: Int = ImageDecoder.ALLOCATOR_DEFAULT
): Drawable? {
return try {
loadDrawableSync(
toImageDecoderSource(source, defaultContext),
maxWidth,
maxHeight,
allocator
)
?:
// If we have a resource, retry fallback using the "normal" Resource loading system.
// This will come into effect in cases like trying to load AnimatedVectorDrawable.
if (source is Res) {
val context = source.context ?: defaultContext
ResourcesCompat.getDrawable(context.resources, source.resId, context.theme)
} else {
null
}
} catch (e: NotFoundException) {
Log.w(TAG, "Couldn't load resource $source", e)
null
}
}
/**
* Loads passed [ImageDecoder.Source] synchronously and returns the drawable.
*
* Maximum height and width can be passed as optional parameters - the image decoder will make
* sure to keep the decoded drawable size within those passed constraints (while keeping aspect
* ratio).
*
* @param maxWidth Maximum width of the returned drawable (if able). 0 means no restriction. Set
* to [DEFAULT_MAX_SAFE_BITMAP_SIZE_PX] by default.
* @param maxHeight Maximum height of the returned drawable (if able). 0 means no restriction.
* Set to [DEFAULT_MAX_SAFE_BITMAP_SIZE_PX] by default.
* @param allocator Allocator to use for the loaded drawable - one of [ImageDecoder] allocator
* ints. Use [ImageDecoder.ALLOCATOR_SOFTWARE] to force software bitmap.
* @return loaded [Drawable] or `null` if loading failed.
*/
@WorkerThread
fun loadDrawableSync(
source: ImageDecoder.Source,
@Px maxWidth: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX,
@Px maxHeight: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX,
allocator: Int = ImageDecoder.ALLOCATOR_DEFAULT
): Drawable? {
return try {
ImageDecoder.decodeDrawable(source) { decoder, info, _ ->
configureDecoderForMaximumSize(decoder, info.size, maxWidth, maxHeight)
decoder.allocator = allocator
}
} catch (e: IOException) {
Log.w(TAG, "Failed to load source $source", e)
return null
} catch (e: DecodeException) {
Log.w(TAG, "Failed to decode source $source", e)
return null
}
}
/** Loads icon drawable while attempting to size restrict the drawable. */
@WorkerThread
fun loadDrawableSync(
icon: Icon,
context: Context = defaultContext,
@Px maxWidth: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX,
@Px maxHeight: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX,
allocator: Int = ImageDecoder.ALLOCATOR_DEFAULT
): Drawable? {
return when (icon.type) {
Icon.TYPE_URI,
Icon.TYPE_URI_ADAPTIVE_BITMAP -> {
val source = ImageDecoder.createSource(context.contentResolver, icon.uri)
loadDrawableSync(source, maxWidth, maxHeight, allocator)
}
Icon.TYPE_RESOURCE -> {
val resources = resolveResourcesForIcon(context, icon)
resources?.let {
loadDrawableSync(
ImageDecoder.createSource(it, icon.resId),
maxWidth,
maxHeight,
allocator
)
}
// Fallback to non-ImageDecoder load if the attempt failed (e.g. the resource
// is a Vector drawable which ImageDecoder doesn't support.)
?: icon.loadDrawable(context)
}
Icon.TYPE_BITMAP -> {
BitmapDrawable(context.resources, icon.bitmap)
}
Icon.TYPE_ADAPTIVE_BITMAP -> {
AdaptiveIconDrawable(null, BitmapDrawable(context.resources, icon.bitmap))
}
Icon.TYPE_DATA -> {
loadDrawableSync(
ImageDecoder.createSource(icon.dataBytes, icon.dataOffset, icon.dataLength),
maxWidth,
maxHeight,
allocator
)
}
else -> {
// We don't recognize this icon, just fallback.
icon.loadDrawable(context)
}
}?.let { drawable ->
// Icons carry tint which we need to propagate down to a Drawable.
tintDrawable(icon, drawable)
drawable
}
}
companion object {
const val TAG = "ImageLoader"
// 4096 is a reasonable default - most devices will support 4096x4096 texture size for
// Canvas rendering and by default we SystemUI has no need to render larger bitmaps.
// This prevents exceptions and crashes if the code accidentally loads larger Bitmap
// and then attempts to render it on Canvas.
// It can always be overridden by the parameters.
const val DEFAULT_MAX_SAFE_BITMAP_SIZE_PX = 4096
/**
* This constant signals that ImageLoader shouldn't attempt to resize the passed bitmap in a
* given dimension.
*
* Set both maxWidth and maxHeight to [DO_NOT_RESIZE] if you wish to prevent resizing.
*/
const val DO_NOT_RESIZE = 0
/** Maps [Source] to [ImageDecoder.Source]. */
private fun toImageDecoderSource(source: Source, defaultContext: Context) =
when (source) {
is Res -> {
val context = source.context ?: defaultContext
ImageDecoder.createSource(context.resources, source.resId)
}
is File -> ImageDecoder.createSource(source.file)
is Uri -> ImageDecoder.createSource(defaultContext.contentResolver, source.uri)
is InputStream -> {
val context = source.context ?: defaultContext
ImageDecoder.createSource(context.resources, source.inputStream)
}
}
/**
* This sets target size on the image decoder to conform to the maxWidth / maxHeight
* parameters. The parameters are chosen to keep the existing drawable aspect ratio.
*/
@AnyThread
private fun configureDecoderForMaximumSize(
decoder: ImageDecoder,
imgSize: Size,
@Px maxWidth: Int,
@Px maxHeight: Int
) {
if (maxWidth == DO_NOT_RESIZE && maxHeight == DO_NOT_RESIZE) {
return
}
if (imgSize.width <= maxWidth && imgSize.height <= maxHeight) {
return
}
// Determine the scale factor for each dimension so it fits within the set constraint
val wScale =
if (maxWidth <= 0) {
1.0f
} else {
maxWidth.toFloat() / imgSize.width.toFloat()
}
val hScale =
if (maxHeight <= 0) {
1.0f
} else {
maxHeight.toFloat() / imgSize.height.toFloat()
}
// Scale down to the dimension that demands larger scaling (smaller scale factor).
// Use the same scale for both dimensions to keep the aspect ratio.
val scale = min(wScale, hScale)
if (scale < 1.0f) {
val targetWidth = (imgSize.width * scale).toInt()
val targetHeight = (imgSize.height * scale).toInt()
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Configured image size to $targetWidth x $targetHeight")
}
decoder.setTargetSize(targetWidth, targetHeight)
}
}
/**
* Attempts to retrieve [Resources] class required to load the passed icon. Icons can
* originate from other processes so we need to make sure we load them from the right
* package source.
*
* @return [Resources] to load the icon drawble or null if icon doesn't carry a resource or
* the resource package couldn't be resolved.
*/
@WorkerThread
private fun resolveResourcesForIcon(context: Context, icon: Icon): Resources? {
if (icon.type != Icon.TYPE_RESOURCE) {
return null
}
val resources = icon.resources
if (resources != null) {
return resources
}
val resPackage = icon.resPackage
if (
resPackage == null || resPackage.isEmpty() || context.packageName.equals(resPackage)
) {
return context.resources
}
if ("android" == resPackage) {
return Resources.getSystem()
}
val pm = context.packageManager
try {
val ai =
pm.getApplicationInfo(
resPackage,
PackageManager.MATCH_UNINSTALLED_PACKAGES or
PackageManager.GET_SHARED_LIBRARY_FILES
)
if (ai != null) {
return pm.getResourcesForApplication(ai)
} else {
Log.w(TAG, "Failed to resolve application info for $resPackage")
}
} catch (e: PackageManager.NameNotFoundException) {
Log.w(TAG, "Failed to resolve resource package", e)
return null
}
return null
}
/** Applies tinting from [Icon] to the passed [Drawable]. */
@AnyThread
private fun tintDrawable(icon: Icon, drawable: Drawable) {
if (icon.hasTint()) {
drawable.mutate()
drawable.setTintList(icon.tintList)
drawable.setTintBlendMode(icon.tintBlendMode)
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 KiB

View File

@@ -0,0 +1,346 @@
package com.android.systemui.graphics
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.ImageDecoder
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.Icon
import android.graphics.drawable.VectorDrawable
import android.net.Uri
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.google.common.truth.Truth.assertThat
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@SmallTest
@kotlinx.coroutines.ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class ImageLoaderTest : SysuiTestCase() {
private val testDispatcher = UnconfinedTestDispatcher()
private val testScope = TestScope(testDispatcher)
private val imageLoader = ImageLoader(context, testDispatcher)
private lateinit var imgFile: File
@Before
fun setUp() {
val context = context.createPackageContext("com.android.systemui.tests", 0)
val bitmap =
BitmapFactory.decodeResource(
context.resources,
com.android.systemui.tests.R.drawable.romainguy_rockaway
)
imgFile = File.createTempFile("image", ".png", context.cacheDir)
imgFile.deleteOnExit()
bitmap.compress(Bitmap.CompressFormat.PNG, 100, FileOutputStream(imgFile))
}
@After
fun tearDown() {
imgFile.delete()
}
@Test
fun invalidResource_drawable_returnsNull() =
testScope.runTest { assertThat(imageLoader.loadDrawable(ImageLoader.Res(-1))).isNull() }
@Test
fun invalidResource_bitmap_returnsNull() =
testScope.runTest { assertThat(imageLoader.loadBitmap(ImageLoader.Res(-1))).isNull() }
@Test
fun invalidUri_returnsNull() =
testScope.runTest {
assertThat(imageLoader.loadBitmap(ImageLoader.Uri("this.is/bogus"))).isNull()
}
@Test
fun invalidFile_returnsNull() =
testScope.runTest {
assertThat(imageLoader.loadBitmap(ImageLoader.File("this is broken!"))).isNull()
}
@Test
fun invalidIcon_returnsNull() =
testScope.runTest {
assertThat(imageLoader.loadDrawable(Icon.createWithFilePath("this is broken"))).isNull()
}
@Test
fun invalidIS_returnsNull() =
testScope.runTest {
assertThat(
imageLoader.loadDrawable(
ImageLoader.InputStream(ByteArrayInputStream(ByteArray(0)))
)
)
.isNull()
}
@Test
fun validBitmapResource_loadDrawable_returnsBitmapDrawable() =
testScope.runTest {
val context = context.createPackageContext("com.android.systemui.tests", 0)
val bitmap =
BitmapFactory.decodeResource(
context.resources,
com.android.systemui.tests.R.drawable.romainguy_rockaway
)
assertThat(bitmap).isNotNull()
val loadedDrawable =
imageLoader.loadDrawable(
ImageLoader.Res(
com.android.systemui.tests.R.drawable.romainguy_rockaway,
context
)
)
assertBitmapEqualToDrawable(loadedDrawable, bitmap)
}
@Test
fun validBitmapResource_loadBitmap_returnsBitmapDrawable() =
testScope.runTest {
val bitmap =
BitmapFactory.decodeResource(
context.resources,
R.drawable.dessert_zombiegingerbread
)
val loadedBitmap =
imageLoader.loadBitmap(ImageLoader.Res(R.drawable.dessert_zombiegingerbread))
assertBitmapEqualToBitmap(loadedBitmap, bitmap)
}
@Test
fun validBitmapUri_returnsBitmapDrawable() =
testScope.runTest {
val bitmap =
BitmapFactory.decodeResource(
context.resources,
R.drawable.dessert_zombiegingerbread
)
val uri =
"android.resource://${context.packageName}/${R.drawable.dessert_zombiegingerbread}"
val loadedBitmap = imageLoader.loadBitmap(ImageLoader.Uri(uri))
assertBitmapEqualToBitmap(loadedBitmap, bitmap)
}
@Test
fun validBitmapFile_returnsBitmapDrawable() =
testScope.runTest {
val bitmap = BitmapFactory.decodeFile(imgFile.absolutePath)
val loadedBitmap = imageLoader.loadBitmap(ImageLoader.File(imgFile))
assertBitmapEqualToBitmap(loadedBitmap, bitmap)
}
@Test
fun validInputStream_returnsBitmapDrawable() =
testScope.runTest {
val bitmap = BitmapFactory.decodeFile(imgFile.absolutePath)
val loadedBitmap =
imageLoader.loadBitmap(ImageLoader.InputStream(FileInputStream(imgFile)))
assertBitmapEqualToBitmap(loadedBitmap, bitmap)
}
@Test
fun validBitmapIcon_returnsBitmapDrawable() =
testScope.runTest {
val bitmap =
BitmapFactory.decodeResource(
context.resources,
R.drawable.dessert_zombiegingerbread
)
val loadedDrawable = imageLoader.loadDrawable(Icon.createWithBitmap(bitmap))
assertBitmapEqualToDrawable(loadedDrawable, bitmap)
}
@Test
fun validUriIcon_returnsBitmapDrawable() =
testScope.runTest {
val bitmap =
BitmapFactory.decodeResource(
context.resources,
R.drawable.dessert_zombiegingerbread
)
val uri =
"android.resource://${context.packageName}/${R.drawable.dessert_zombiegingerbread}"
val loadedDrawable = imageLoader.loadDrawable(Icon.createWithContentUri(Uri.parse(uri)))
assertBitmapEqualToDrawable(loadedDrawable, bitmap)
}
@Test
fun validDataIcon_returnsBitmapDrawable() =
testScope.runTest {
val bitmap =
BitmapFactory.decodeResource(
context.resources,
R.drawable.dessert_zombiegingerbread
)
val bos =
ByteArrayOutputStream(
bitmap.byteCount * 2
) // Compressed bitmap should be smaller than its source.
bitmap.compress(Bitmap.CompressFormat.PNG, 100, bos)
val array = bos.toByteArray()
val loadedDrawable = imageLoader.loadDrawable(Icon.createWithData(array, 0, array.size))
assertBitmapEqualToDrawable(loadedDrawable, bitmap)
}
@Test
fun validSystemResourceIcon_returnsBitmapDrawable() =
testScope.runTest {
val bitmap =
Resources.getSystem().getDrawable(android.R.drawable.ic_dialog_alert, context.theme)
val loadedDrawable =
imageLoader.loadDrawable(
Icon.createWithResource("android", android.R.drawable.ic_dialog_alert)
)
assertBitmapEqualToDrawable(loadedDrawable, (bitmap as BitmapDrawable).bitmap)
}
@Test
fun invalidDifferentPackageResourceIcon_returnsNull() =
testScope.runTest {
val loadedDrawable =
imageLoader.loadDrawable(
Icon.createWithResource(
"noooope.wrong.package",
R.drawable.dessert_zombiegingerbread
)
)
assertThat(loadedDrawable).isNull()
}
@Test
fun validBitmapResource_widthMoreRestricted_downsizesKeepingAspectRatio() =
testScope.runTest {
val loadedDrawable =
imageLoader.loadDrawable(ImageLoader.File(imgFile), maxWidth = 160, maxHeight = 160)
val loadedBitmap = assertBitmapInDrawable(loadedDrawable)
assertThat(loadedBitmap.width).isEqualTo(160)
assertThat(loadedBitmap.height).isEqualTo(106)
}
@Test
fun validBitmapResource_heightMoreRestricted_downsizesKeepingAspectRatio() =
testScope.runTest {
val loadedDrawable =
imageLoader.loadDrawable(ImageLoader.File(imgFile), maxWidth = 160, maxHeight = 50)
val loadedBitmap = assertBitmapInDrawable(loadedDrawable)
assertThat(loadedBitmap.width).isEqualTo(74)
assertThat(loadedBitmap.height).isEqualTo(50)
}
@Test
fun validBitmapResource_onlyWidthRestricted_downsizesKeepingAspectRatio() =
testScope.runTest {
val loadedDrawable =
imageLoader.loadDrawable(
ImageLoader.File(imgFile),
maxWidth = 160,
maxHeight = ImageLoader.DO_NOT_RESIZE
)
val loadedBitmap = assertBitmapInDrawable(loadedDrawable)
assertThat(loadedBitmap.width).isEqualTo(160)
assertThat(loadedBitmap.height).isEqualTo(106)
}
@Test
fun validBitmapResource_onlyHeightRestricted_downsizesKeepingAspectRatio() =
testScope.runTest {
val loadedDrawable =
imageLoader.loadDrawable(
ImageLoader.Res(R.drawable.bubble_thumbnail),
maxWidth = ImageLoader.DO_NOT_RESIZE,
maxHeight = 120
)
val loadedBitmap = assertBitmapInDrawable(loadedDrawable)
assertThat(loadedBitmap.width).isEqualTo(123)
assertThat(loadedBitmap.height).isEqualTo(120)
}
@Test
fun validVectorDrawable_loadDrawable_successfullyLoaded() =
testScope.runTest {
val loadedDrawable = imageLoader.loadDrawable(ImageLoader.Res(R.drawable.ic_settings))
assertThat(loadedDrawable).isNotNull()
assertThat(loadedDrawable).isInstanceOf(VectorDrawable::class.java)
}
@Test
fun validVectorDrawable_loadBitmap_returnsNull() =
testScope.runTest {
val loadedBitmap = imageLoader.loadBitmap(ImageLoader.Res(R.drawable.ic_settings))
assertThat(loadedBitmap).isNull()
}
@Test
fun validVectorDrawableIcon_loadDrawable_successfullyLoaded() =
testScope.runTest {
val loadedDrawable =
imageLoader.loadDrawable(Icon.createWithResource(context, R.drawable.ic_settings))
assertThat(loadedDrawable).isNotNull()
assertThat(loadedDrawable).isInstanceOf(VectorDrawable::class.java)
}
@Test
fun hardwareAllocator_returnsHardwareBitmap() =
testScope.runTest {
val loadedDrawable =
imageLoader.loadDrawable(
ImageLoader.File(imgFile),
allocator = ImageDecoder.ALLOCATOR_HARDWARE
)
assertThat(loadedDrawable).isNotNull()
assertThat((loadedDrawable as BitmapDrawable).bitmap.config)
.isEqualTo(Bitmap.Config.HARDWARE)
}
@Test
fun softwareAllocator_returnsSoftwareBitmap() =
testScope.runTest {
val loadedDrawable =
imageLoader.loadDrawable(
ImageLoader.File(imgFile),
allocator = ImageDecoder.ALLOCATOR_SOFTWARE
)
assertThat(loadedDrawable).isNotNull()
assertThat((loadedDrawable as BitmapDrawable).bitmap.config)
.isNotEqualTo(Bitmap.Config.HARDWARE)
}
private fun assertBitmapInDrawable(drawable: Drawable?): Bitmap {
assertThat(drawable).isNotNull()
assertThat(drawable).isInstanceOf(BitmapDrawable::class.java)
return (drawable as BitmapDrawable).bitmap
}
private fun assertBitmapEqualToDrawable(actual: Drawable?, expected: Bitmap) {
val actualBitmap = assertBitmapInDrawable(actual)
assertBitmapEqualToBitmap(actualBitmap, expected)
}
private fun assertBitmapEqualToBitmap(actual: Bitmap?, expected: Bitmap) {
assertThat(actual).isNotNull()
assertThat(actual?.width).isEqualTo(expected.width)
assertThat(actual?.height).isEqualTo(expected.height)
}
}