1 /*
2  * 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                 ?: icon.loadDrawable(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                 icon.loadDrawable(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     companion object {
370         const val TAG = "ImageLoader"
371 
372         // 4096 is a reasonable default - most devices will support 4096x4096 texture size for
373         // Canvas rendering and by default we SystemUI has no need to render larger bitmaps.
374         // This prevents exceptions and crashes if the code accidentally loads larger Bitmap
375         // and then attempts to render it on Canvas.
376         // It can always be overridden by the parameters.
377         const val DEFAULT_MAX_SAFE_BITMAP_SIZE_PX = 4096
378 
379         /**
380          * This constant signals that ImageLoader shouldn't attempt to resize the passed bitmap in a
381          * given dimension.
382          *
383          * Set both maxWidth and maxHeight to [DO_NOT_RESIZE] if you wish to prevent resizing.
384          */
385         const val DO_NOT_RESIZE = 0
386 
387         /** Maps [Source] to [ImageDecoder.Source]. */
388         private fun toImageDecoderSource(source: Source, defaultContext: Context) =
389             when (source) {
390                 is Res -> {
391                     val context = source.context ?: defaultContext
392                     ImageDecoder.createSource(context.resources, source.resId)
393                 }
394                 is File -> ImageDecoder.createSource(source.file)
395                 is Uri -> ImageDecoder.createSource(defaultContext.contentResolver, source.uri)
396                 is InputStream -> {
397                     val context = source.context ?: defaultContext
398                     ImageDecoder.createSource(context.resources, source.inputStream)
399                 }
400             }
401 
402         /**
403          * This sets target size on the image decoder to conform to the maxWidth / maxHeight
404          * parameters. The parameters are chosen to keep the existing drawable aspect ratio.
405          */
406         @AnyThread
407         private fun configureDecoderForMaximumSize(
408             decoder: ImageDecoder,
409             imgSize: Size,
410             @Px maxWidth: Int,
411             @Px maxHeight: Int
412         ) {
413             if (maxWidth == DO_NOT_RESIZE && maxHeight == DO_NOT_RESIZE) {
414                 return
415             }
416 
417             if (imgSize.width <= maxWidth && imgSize.height <= maxHeight) {
418                 return
419             }
420 
421             // Determine the scale factor for each dimension so it fits within the set constraint
422             val wScale =
423                 if (maxWidth <= 0) {
424                     1.0f
425                 } else {
426                     maxWidth.toFloat() / imgSize.width.toFloat()
427                 }
428 
429             val hScale =
430                 if (maxHeight <= 0) {
431                     1.0f
432                 } else {
433                     maxHeight.toFloat() / imgSize.height.toFloat()
434                 }
435 
436             // Scale down to the dimension that demands larger scaling (smaller scale factor).
437             // Use the same scale for both dimensions to keep the aspect ratio.
438             val scale = min(wScale, hScale)
439             if (scale < 1.0f) {
440                 val targetWidth = (imgSize.width * scale).toInt()
441                 val targetHeight = (imgSize.height * scale).toInt()
442                 if (Log.isLoggable(TAG, Log.DEBUG)) {
443                     Log.d(TAG, "Configured image size to $targetWidth x $targetHeight")
444                 }
445 
446                 decoder.setTargetSize(targetWidth, targetHeight)
447             }
448         }
449 
450         /**
451          * Attempts to retrieve [Resources] class required to load the passed icon. Icons can
452          * originate from other processes so we need to make sure we load them from the right
453          * package source.
454          *
455          * @return [Resources] to load the icon drawble or null if icon doesn't carry a resource or
456          *   the resource package couldn't be resolved.
457          */
458         @WorkerThread
459         private fun resolveResourcesForIcon(context: Context, icon: Icon): Resources? {
460             if (icon.type != Icon.TYPE_RESOURCE) {
461                 return null
462             }
463 
464             val resources = icon.resources
465             if (resources != null) {
466                 return resources
467             }
468 
469             val resPackage = icon.resPackage
470             if (
471                 resPackage == null || resPackage.isEmpty() || context.packageName.equals(resPackage)
472             ) {
473                 return context.resources
474             }
475 
476             if ("android" == resPackage) {
477                 return Resources.getSystem()
478             }
479 
480             val pm = context.packageManager
481             try {
482                 val ai =
483                     pm.getApplicationInfo(
484                         resPackage,
485                         PackageManager.MATCH_UNINSTALLED_PACKAGES or
486                             PackageManager.GET_SHARED_LIBRARY_FILES
487                     )
488                 if (ai != null) {
489                     return pm.getResourcesForApplication(ai)
490                 } else {
491                     Log.w(TAG, "Failed to resolve application info for $resPackage")
492                 }
493             } catch (e: PackageManager.NameNotFoundException) {
494                 Log.w(TAG, "Failed to resolve resource package", e)
495                 return null
496             }
497             return null
498         }
499 
500         /** Applies tinting from [Icon] to the passed [Drawable]. */
501         @AnyThread
502         private fun tintDrawable(icon: Icon, drawable: Drawable) {
503             if (icon.hasTint()) {
504                 drawable.mutate()
505                 drawable.setTintList(icon.tintList)
506                 drawable.setTintBlendMode(icon.tintBlendMode)
507             }
508         }
509     }
510 }
511