Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 9b5151b2 authored by Jernej Virag's avatar Jernej Virag Committed by Automerger Merge Worker
Browse files

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

parents 1009c752 0c5af730
Loading
Loading
Loading
Loading
+493 −0
Original line number Original line Diff line number Diff line
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)
            }
        }
    }
}
+405 KiB
Loading image diff...
+346 −0

File added.

Preview size limit exceeded, changes collapsed.