• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.graphics
18 
19 import android.annotation.AnyThread
20 import android.annotation.DrawableRes
21 import android.annotation.Px
22 import android.annotation.SuppressLint
23 import android.annotation.WorkerThread
24 import android.content.Context
25 import android.content.pm.PackageManager
26 import android.content.res.Resources
27 import android.content.res.Resources.NotFoundException
28 import android.graphics.Bitmap
29 import android.graphics.ImageDecoder
30 import android.graphics.drawable.AdaptiveIconDrawable
31 import android.graphics.drawable.BitmapDrawable
32 import android.graphics.drawable.Drawable
33 import android.graphics.drawable.Icon
34 import android.util.Log
35 import android.util.Size
36 import androidx.core.content.res.ResourcesCompat
37 import com.android.app.tracing.traceSection
38 import com.android.systemui.dagger.SysUISingleton
39 import com.android.systemui.dagger.qualifiers.Application
40 import com.android.systemui.dagger.qualifiers.Background
41 import javax.inject.Inject
42 import kotlin.math.min
43 import kotlinx.coroutines.CoroutineDispatcher
44 import kotlinx.coroutines.withContext
45 
46 /**
47  * Helper class to load images for SystemUI. It allows for memory efficient image loading with size
48  * restriction and attempts to use hardware bitmaps when sensible.
49  */
50 @SysUISingleton
51 class ImageLoader
52 @Inject
53 constructor(
54     @Application private val defaultContext: Context,
55     @Background private val backgroundDispatcher: CoroutineDispatcher,
56 ) {
57 
58     /** Source of the image data. */
59     sealed interface Source
60 
61     /**
62      * Load image from a Resource ID. If the resource is part of another package or if it requires
63      * tinting, pass in a correct [Context].
64      */
65     data class Res(@DrawableRes val resId: Int, val context: Context?) : Source {
66         constructor(@DrawableRes resId: Int) : this(resId, null)
67     }
68 
69     /** Load image from a Uri. */
70     data class Uri(val uri: android.net.Uri) : Source {
71         constructor(uri: String) : this(android.net.Uri.parse(uri))
72     }
73 
74     /** Load image from a [File]. */
75     data class File(val file: java.io.File) : Source {
76         constructor(path: String) : this(java.io.File(path))
77     }
78 
79     /** Load image from an [InputStream]. */
80     data class InputStream(val inputStream: java.io.InputStream, val context: Context?) : Source {
81         constructor(inputStream: java.io.InputStream) : this(inputStream, null)
82     }
83 
84     /**
85      * Loads passed [Source] on a background thread and returns the [Bitmap].
86      *
87      * Maximum height and width can be passed as optional parameters - the image decoder will make
88      * sure to keep the decoded drawable size within those passed constraints while keeping aspect
89      * ratio.
90      *
91      * @param maxWidth Maximum width of the returned drawable (if able). 0 means no restriction. Set
92      *   to [DEFAULT_MAX_SAFE_BITMAP_SIZE_PX] by default.
93      * @param maxHeight Maximum height of the returned drawable (if able). 0 means no restriction.
94      *   Set to [DEFAULT_MAX_SAFE_BITMAP_SIZE_PX] by default.
95      * @param allocator Allocator to use for the loaded drawable - one of [ImageDecoder] allocator
96      *   ints. Use [ImageDecoder.ALLOCATOR_SOFTWARE] to force software bitmap.
97      * @return loaded [Bitmap] or `null` if loading failed.
98      */
99     @AnyThread
100     suspend fun loadBitmap(
101         source: Source,
102         @Px maxWidth: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX,
103         @Px maxHeight: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX,
104         allocator: Int = ImageDecoder.ALLOCATOR_DEFAULT,
105     ): Bitmap? =
106         withContext(backgroundDispatcher) { loadBitmapSync(source, maxWidth, maxHeight, allocator) }
107 
108     /**
109      * Loads passed [Source] synchronously and returns the [Bitmap].
110      *
111      * Maximum height and width can be passed as optional parameters - the image decoder will make
112      * sure to keep the decoded drawable size within those passed constraints while keeping aspect
113      * ratio.
114      *
115      * @param maxWidth Maximum width of the returned drawable (if able). 0 means no restriction. Set
116      *   to [DEFAULT_MAX_SAFE_BITMAP_SIZE_PX] by default.
117      * @param maxHeight Maximum height of the returned drawable (if able). 0 means no restriction.
118      *   Set to [DEFAULT_MAX_SAFE_BITMAP_SIZE_PX] by default.
119      * @param allocator Allocator to use for the loaded drawable - one of [ImageDecoder] allocator
120      *   ints. Use [ImageDecoder.ALLOCATOR_SOFTWARE] to force software bitmap.
121      * @return loaded [Bitmap] or `null` if loading failed.
122      */
123     @WorkerThread
124     fun loadBitmapSync(
125         source: Source,
126         @Px maxWidth: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX,
127         @Px maxHeight: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX,
128         allocator: Int = ImageDecoder.ALLOCATOR_DEFAULT,
129     ): Bitmap? {
130         return try {
131             loadBitmapSync(
132                 toImageDecoderSource(source, defaultContext),
133                 maxWidth,
134                 maxHeight,
135                 allocator,
136             )
137         } catch (e: NotFoundException) {
138             Log.w(TAG, "Couldn't load resource $source", e)
139             null
140         }
141     }
142 
143     /**
144      * Loads passed [ImageDecoder.Source] synchronously and returns the drawable.
145      *
146      * Maximum height and width can be passed as optional parameters - the image decoder will make
147      * sure to keep the decoded drawable size within those passed constraints (while keeping aspect
148      * ratio).
149      *
150      * @param maxWidth Maximum width of the returned drawable (if able). 0 means no restriction. Set
151      *   to [DEFAULT_MAX_SAFE_BITMAP_SIZE_PX] by default.
152      * @param maxHeight Maximum height of the returned drawable (if able). 0 means no restriction.
153      *   Set to [DEFAULT_MAX_SAFE_BITMAP_SIZE_PX] by default.
154      * @param allocator Allocator to use for the loaded drawable - one of [ImageDecoder] allocator
155      *   ints. Use [ImageDecoder.ALLOCATOR_SOFTWARE] to force software bitmap.
156      * @return loaded [Bitmap] or `null` if loading failed.
157      */
158     @WorkerThread
159     fun loadBitmapSync(
160         source: ImageDecoder.Source,
161         @Px maxWidth: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX,
162         @Px maxHeight: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX,
163         allocator: Int = ImageDecoder.ALLOCATOR_DEFAULT,
164     ): Bitmap? =
165         traceSection("ImageLoader#loadBitmap") {
166             return try {
167                 ImageDecoder.decodeBitmap(source) { decoder, info, _ ->
168                     configureDecoderForMaximumSize(decoder, info.size, maxWidth, maxHeight)
169                     decoder.allocator = allocator
170                 }
171             } catch (e: Exception) {
172                 // If we're loading an Uri, we can receive any exception from the other side.
173                 // So we have to catch them all.
174                 Log.w(TAG, "Failed to load source $source", e)
175                 return null
176             }
177         }
178 
179     /**
180      * Loads passed [Source] on a background thread and returns the [Drawable].
181      *
182      * Maximum height and width can be passed as optional parameters - the image decoder will make
183      * sure to keep the decoded drawable size within those passed constraints (while keeping aspect
184      * ratio).
185      *
186      * @param maxWidth Maximum width of the returned drawable (if able). 0 means no restriction. Set
187      *   to [DEFAULT_MAX_SAFE_BITMAP_SIZE_PX] by default.
188      * @param maxHeight Maximum height of the returned drawable (if able). 0 means no restriction.
189      *   Set to [DEFAULT_MAX_SAFE_BITMAP_SIZE_PX] by default.
190      * @param allocator Allocator to use for the loaded drawable - one of [ImageDecoder] allocator
191      *   ints. Use [ImageDecoder.ALLOCATOR_SOFTWARE] to force software bitmap.
192      * @return loaded [Drawable] or `null` if loading failed.
193      */
194     @AnyThread
195     suspend fun loadDrawable(
196         source: Source,
197         @Px maxWidth: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX,
198         @Px maxHeight: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX,
199         allocator: Int = ImageDecoder.ALLOCATOR_DEFAULT,
200     ): Drawable? =
201         withContext(backgroundDispatcher) {
202             loadDrawableSync(source, maxWidth, maxHeight, allocator)
203         }
204 
205     /**
206      * Loads passed [Icon] on a background thread and returns the drawable.
207      *
208      * Maximum height and width can be passed as optional parameters - the image decoder will make
209      * sure to keep the decoded drawable size within those passed constraints (while keeping aspect
210      * ratio).
211      *
212      * @param context Alternate context to use for resource loading (for e.g. cross-process use)
213      * @param maxWidth Maximum width of the returned drawable (if able). 0 means no restriction. Set
214      *   to [DEFAULT_MAX_SAFE_BITMAP_SIZE_PX] by default.
215      * @param maxHeight Maximum height of the returned drawable (if able). 0 means no restriction.
216      *   Set to [DEFAULT_MAX_SAFE_BITMAP_SIZE_PX] by default.
217      * @param allocator Allocator to use for the loaded drawable - one of [ImageDecoder] allocator
218      *   ints. Use [ImageDecoder.ALLOCATOR_SOFTWARE] to force software bitmap.
219      * @return loaded [Drawable] or `null` if loading failed.
220      */
221     @AnyThread
222     suspend fun loadDrawable(
223         icon: Icon,
224         context: Context = defaultContext,
225         @Px maxWidth: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX,
226         @Px maxHeight: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX,
227         allocator: Int = ImageDecoder.ALLOCATOR_DEFAULT,
228     ): Drawable? =
229         withContext(backgroundDispatcher) {
230             loadDrawableSync(icon, context, maxWidth, maxHeight, allocator)
231         }
232 
233     /**
234      * Loads passed [Source] synchronously and returns the drawable.
235      *
236      * Maximum height and width can be passed as optional parameters - the image decoder will make
237      * sure to keep the decoded drawable size within those passed constraints (while keeping aspect
238      * ratio).
239      *
240      * @param maxWidth Maximum width of the returned drawable (if able). 0 means no restriction. Set
241      *   to [DEFAULT_MAX_SAFE_BITMAP_SIZE_PX] by default.
242      * @param maxHeight Maximum height of the returned drawable (if able). 0 means no restriction.
243      *   Set to [DEFAULT_MAX_SAFE_BITMAP_SIZE_PX] by default.
244      * @param allocator Allocator to use for the loaded drawable - one of [ImageDecoder] allocator
245      *   ints. Use [ImageDecoder.ALLOCATOR_SOFTWARE] to force software bitmap.
246      * @return loaded [Drawable] or `null` if loading failed.
247      */
248     @WorkerThread
249     @SuppressLint("UseCompatLoadingForDrawables")
250     fun loadDrawableSync(
251         source: Source,
252         @Px maxWidth: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX,
253         @Px maxHeight: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX,
254         allocator: Int = ImageDecoder.ALLOCATOR_DEFAULT,
255     ): Drawable? =
256         traceSection("ImageLoader#loadDrawable") {
257             return try {
258                 loadDrawableSync(
259                     toImageDecoderSource(source, defaultContext),
260                     maxWidth,
261                     maxHeight,
262                     allocator,
263                 )
264                     ?:
265                     // If we have a resource, retry fallback using the "normal" Resource loading
266                     // system.
267                     // This will come into effect in cases like trying to load
268                     // AnimatedVectorDrawable.
269                     if (source is Res) {
270                         val context = source.context ?: defaultContext
271                         ResourcesCompat.getDrawable(context.resources, source.resId, context.theme)
272                     } else {
273                         null
274                     }
275             } catch (e: NotFoundException) {
276                 Log.w(TAG, "Couldn't load resource $source", e)
277                 null
278             }
279         }
280 
281     /**
282      * Loads passed [ImageDecoder.Source] synchronously and returns the drawable.
283      *
284      * Maximum height and width can be passed as optional parameters - the image decoder will make
285      * sure to keep the decoded drawable size within those passed constraints (while keeping aspect
286      * ratio).
287      *
288      * @param maxWidth Maximum width of the returned drawable (if able). 0 means no restriction. Set
289      *   to [DEFAULT_MAX_SAFE_BITMAP_SIZE_PX] by default.
290      * @param maxHeight Maximum height of the returned drawable (if able). 0 means no restriction.
291      *   Set to [DEFAULT_MAX_SAFE_BITMAP_SIZE_PX] by default.
292      * @param allocator Allocator to use for the loaded drawable - one of [ImageDecoder] allocator
293      *   ints. Use [ImageDecoder.ALLOCATOR_SOFTWARE] to force software bitmap.
294      * @return loaded [Drawable] or `null` if loading failed.
295      */
296     @WorkerThread
297     fun loadDrawableSync(
298         source: ImageDecoder.Source,
299         @Px maxWidth: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX,
300         @Px maxHeight: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX,
301         allocator: Int = ImageDecoder.ALLOCATOR_DEFAULT,
302     ): Drawable? =
303         traceSection("ImageLoader#loadDrawable") {
304             return try {
305                 ImageDecoder.decodeDrawable(source) { decoder, info, _ ->
306                     configureDecoderForMaximumSize(decoder, info.size, maxWidth, maxHeight)
307                     decoder.allocator = allocator
308                 }
309             } catch (e: Exception) {
310                 // If we're loading from an Uri, any exception can happen on the
311                 // other side. We have to catch them all.
312                 Log.w(TAG, "Failed to load source $source", e)
313                 return null
314             }
315         }
316 
317     /** Loads icon drawable while attempting to size restrict the drawable. */
318     @WorkerThread
319     fun loadDrawableSync(
320         icon: Icon,
321         context: Context = defaultContext,
322         @Px maxWidth: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX,
323         @Px maxHeight: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX,
324         allocator: Int = ImageDecoder.ALLOCATOR_DEFAULT,
325     ): Drawable? =
326         traceSection("ImageLoader#loadDrawable") {
327             return when (icon.type) {
328                 Icon.TYPE_URI,
329                 Icon.TYPE_URI_ADAPTIVE_BITMAP -> {
330                     val source = ImageDecoder.createSource(context.contentResolver, icon.uri)
331                     loadDrawableSync(source, maxWidth, maxHeight, allocator)
332                 }
333                 Icon.TYPE_RESOURCE -> {
334                     val resources = resolveResourcesForIcon(context, icon)
335                     resources?.let {
336                         loadDrawableSync(
337                             ImageDecoder.createSource(it, icon.resId),
338                             maxWidth,
339                             maxHeight,
340                             allocator,
341                         )
342                     }
343                         // Fallback to non-ImageDecoder load if the attempt failed (e.g. the
344                         // resource
345                         // is a Vector drawable which ImageDecoder doesn't support.)
346                         ?: loadIconDrawable(icon, context)
347                 }
348                 Icon.TYPE_BITMAP -> {
349                     BitmapDrawable(context.resources, icon.bitmap)
350                 }
351                 Icon.TYPE_ADAPTIVE_BITMAP -> {
352                     AdaptiveIconDrawable(null, BitmapDrawable(context.resources, icon.bitmap))
353                 }
354                 Icon.TYPE_DATA -> {
355                     loadDrawableSync(
356                         ImageDecoder.createSource(icon.dataBytes, icon.dataOffset, icon.dataLength),
357                         maxWidth,
358                         maxHeight,
359                         allocator,
360                     )
361                 }
362                 else -> {
363                     // We don't recognize this icon, just fallback.
364                     loadIconDrawable(icon, context)
365                 }
366             }?.let { drawable ->
367                 // Icons carry tint which we need to propagate down to a Drawable.
368                 tintDrawable(icon, drawable)
369                 drawable
370             }
371         }
372 
373     @WorkerThread
374     fun loadIconDrawable(icon: Icon, context: Context): Drawable? {
375         icon.loadDrawable(context)?.let {
376             return it
377         }
378 
379         Log.w(TAG, "Failed to load drawable for $icon")
380         return null
381     }
382 
383     /**
384      * Obtains the image size from the image header, without decoding the full image.
385      *
386      * @param icon an [Icon] representing the source of the image
387      * @return the [Size] if it could be determined from the image header, or `null` otherwise
388      */
389     suspend fun loadSize(icon: Icon, context: Context): Size? =
390         withContext(backgroundDispatcher) { loadSizeSync(icon, context) }
391 
392     /**
393      * Obtains the image size from the image header, without decoding the full image.
394      *
395      * @param icon an [Icon] representing the source of the image
396      * @return the [Size] if it could be determined from the image header, or `null` otherwise
397      */
398     @WorkerThread
399     fun loadSizeSync(icon: Icon, context: Context): Size? {
400         return when (icon.type) {
401             Icon.TYPE_URI,
402             Icon.TYPE_URI_ADAPTIVE_BITMAP -> {
403                 val source = ImageDecoder.createSource(context.contentResolver, icon.uri)
404                 loadSizeSync(source)
405             }
406             else -> null
407         }
408     }
409 
410     /**
411      * Obtains the image size from the image header, without decoding the full image.
412      *
413      * @param source [ImageDecoder.Source] of the image
414      * @return the [Size] if it could be determined from the image header, or `null` otherwise
415      */
416     @WorkerThread
417     fun loadSizeSync(source: ImageDecoder.Source): Size? {
418         return try {
419             ImageDecoder.decodeHeader(source).size
420         } catch (e: Exception) {
421             // Any exception can happen when loading Uris, so we have to catch them all.
422             Log.w(TAG, "Failed to load source $source", e)
423             return null
424         }
425     }
426 
427     companion object {
428         const val TAG = "ImageLoader"
429 
430         // 4096 is a reasonable default - most devices will support 4096x4096 texture size for
431         // Canvas rendering and by default we SystemUI has no need to render larger bitmaps.
432         // This prevents exceptions and crashes if the code accidentally loads larger Bitmap
433         // and then attempts to render it on Canvas.
434         // It can always be overridden by the parameters.
435         const val DEFAULT_MAX_SAFE_BITMAP_SIZE_PX = 4096
436 
437         /**
438          * This constant signals that ImageLoader shouldn't attempt to resize the passed bitmap in a
439          * given dimension.
440          *
441          * Set both maxWidth and maxHeight to [DO_NOT_RESIZE] if you wish to prevent resizing.
442          */
443         const val DO_NOT_RESIZE = 0
444 
445         /** Maps [Source] to [ImageDecoder.Source]. */
446         private fun toImageDecoderSource(source: Source, defaultContext: Context) =
447             when (source) {
448                 is Res -> {
449                     val context = source.context ?: defaultContext
450                     ImageDecoder.createSource(context.resources, source.resId)
451                 }
452                 is File -> ImageDecoder.createSource(source.file)
453                 is Uri -> ImageDecoder.createSource(defaultContext.contentResolver, source.uri)
454                 is InputStream -> {
455                     val context = source.context ?: defaultContext
456                     ImageDecoder.createSource(context.resources, source.inputStream)
457                 }
458             }
459 
460         /**
461          * This sets target size on the image decoder to conform to the maxWidth / maxHeight
462          * parameters. The parameters are chosen to keep the existing drawable aspect ratio.
463          */
464         @AnyThread
465         private fun configureDecoderForMaximumSize(
466             decoder: ImageDecoder,
467             imgSize: Size,
468             @Px maxWidth: Int,
469             @Px maxHeight: Int,
470         ) {
471             if (maxWidth == DO_NOT_RESIZE && maxHeight == DO_NOT_RESIZE) {
472                 return
473             }
474 
475             if (imgSize.width <= maxWidth && imgSize.height <= maxHeight) {
476                 return
477             }
478 
479             // Determine the scale factor for each dimension so it fits within the set constraint
480             val wScale =
481                 if (maxWidth <= 0) {
482                     1.0f
483                 } else {
484                     maxWidth.toFloat() / imgSize.width.toFloat()
485                 }
486 
487             val hScale =
488                 if (maxHeight <= 0) {
489                     1.0f
490                 } else {
491                     maxHeight.toFloat() / imgSize.height.toFloat()
492                 }
493 
494             // Scale down to the dimension that demands larger scaling (smaller scale factor).
495             // Use the same scale for both dimensions to keep the aspect ratio.
496             val scale = min(wScale, hScale)
497             if (scale < 1.0f) {
498                 val targetWidth = (imgSize.width * scale).toInt()
499                 val targetHeight = (imgSize.height * scale).toInt()
500                 if (Log.isLoggable(TAG, Log.DEBUG)) {
501                     Log.d(TAG, "Configured image size to $targetWidth x $targetHeight")
502                 }
503 
504                 decoder.setTargetSize(targetWidth, targetHeight)
505             }
506         }
507 
508         /**
509          * Attempts to retrieve [Resources] class required to load the passed icon. Icons can
510          * originate from other processes so we need to make sure we load them from the right
511          * package source.
512          *
513          * @return [Resources] to load the icon drawable or null if icon doesn't carry a resource or
514          *   the resource package couldn't be resolved.
515          */
516         @WorkerThread
517         private fun resolveResourcesForIcon(context: Context, icon: Icon): Resources? {
518             if (icon.type != Icon.TYPE_RESOURCE) {
519                 return null
520             }
521 
522             val resources = icon.resources
523             if (resources != null) {
524                 return resources
525             }
526 
527             val resPackage = icon.resPackage
528             if (
529                 resPackage == null || resPackage.isEmpty() || context.packageName.equals(resPackage)
530             ) {
531                 return context.resources
532             }
533 
534             if ("android" == resPackage) {
535                 return Resources.getSystem()
536             }
537 
538             val pm = context.packageManager
539             try {
540                 val ai =
541                     pm.getApplicationInfo(
542                         resPackage,
543                         PackageManager.MATCH_UNINSTALLED_PACKAGES or
544                             PackageManager.GET_SHARED_LIBRARY_FILES,
545                     )
546                 if (ai != null) {
547                     return pm.getResourcesForApplication(ai)
548                 } else {
549                     Log.w(TAG, "Failed to resolve application info for $resPackage")
550                 }
551             } catch (e: PackageManager.NameNotFoundException) {
552                 Log.w(TAG, "Failed to resolve resource package", e)
553                 return null
554             }
555             return null
556         }
557 
558         /** Applies tinting from [Icon] to the passed [Drawable]. */
559         @AnyThread
560         private fun tintDrawable(icon: Icon, drawable: Drawable) {
561             if (icon.hasTint()) {
562                 drawable.mutate()
563                 drawable.setTintList(icon.tintList)
564                 drawable.setTintBlendMode(icon.tintBlendMode)
565             }
566         }
567     }
568 }
569