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