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