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