Merge "Implement common ImageLoader in SystemUI" into udc-dev
This commit is contained in:
@@ -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 |
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user