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