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