1 /* <lambda>null2 * Copyright (C) 2024 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.intentresolver.contentpreview 18 19 import android.graphics.Bitmap 20 import android.net.Uri 21 import android.util.Log 22 import android.util.Size 23 import androidx.core.util.lruCache 24 import com.android.intentresolver.inject.Background 25 import com.android.intentresolver.inject.ViewModelOwned 26 import javax.inject.Inject 27 import javax.inject.Qualifier 28 import kotlinx.coroutines.CoroutineDispatcher 29 import kotlinx.coroutines.CoroutineScope 30 import kotlinx.coroutines.Deferred 31 import kotlinx.coroutines.ExperimentalCoroutinesApi 32 import kotlinx.coroutines.async 33 import kotlinx.coroutines.ensureActive 34 import kotlinx.coroutines.sync.Semaphore 35 import kotlinx.coroutines.sync.withPermit 36 import kotlinx.coroutines.withContext 37 38 @Qualifier 39 @MustBeDocumented 40 @Retention(AnnotationRetention.BINARY) 41 annotation class PreviewMaxConcurrency 42 43 /** 44 * Implementation of [ImageLoader]. 45 * 46 * Allows for cached or uncached loading of images and limits the number of concurrent requests. 47 * Requests are automatically cancelled when they are evicted from the cache. If image loading fails 48 * or the request is cancelled (e.g. by eviction), the returned [Bitmap] will be null. 49 */ 50 class CachingImagePreviewImageLoader 51 @Inject 52 constructor( 53 @ViewModelOwned private val scope: CoroutineScope, 54 @Background private val bgDispatcher: CoroutineDispatcher, 55 private val thumbnailLoader: ThumbnailLoader, 56 @PreviewCacheSize cacheSize: Int, 57 @PreviewMaxConcurrency maxConcurrency: Int, 58 ) : ImageLoader { 59 60 private val semaphore = Semaphore(maxConcurrency) 61 62 private val cache = 63 lruCache( 64 maxSize = cacheSize, 65 create = { uri: Uri -> scope.async { loadUncachedImage(uri) } }, 66 onEntryRemoved = { evicted: Boolean, _, oldValue: Deferred<Bitmap?>, _ -> 67 // If removed due to eviction, cancel the coroutine, otherwise it is the 68 // responsibility 69 // of the caller of [cache.remove] to cancel the removed entry when done with it. 70 if (evicted) { 71 oldValue.cancel() 72 } 73 } 74 ) 75 76 override fun prePopulate(uriSizePairs: List<Pair<Uri, Size>>) { 77 uriSizePairs.take(cache.maxSize()).map { cache[it.first] } 78 } 79 80 override suspend fun invoke(uri: Uri, size: Size, caching: Boolean): Bitmap? { 81 return if (caching) { 82 loadCachedImage(uri) 83 } else { 84 loadUncachedImage(uri) 85 } 86 } 87 88 private suspend fun loadUncachedImage(uri: Uri): Bitmap? = 89 withContext(bgDispatcher) { 90 runCatching { semaphore.withPermit { thumbnailLoader.loadThumbnail(uri) } } 91 .onFailure { 92 ensureActive() 93 Log.d(TAG, "Failed to load preview for $uri", it) 94 } 95 .getOrNull() 96 } 97 98 private suspend fun loadCachedImage(uri: Uri): Bitmap? = 99 // [Deferred#await] is called in a [runCatching] block to catch 100 // [CancellationExceptions]s so that they don't cancel the calling coroutine/scope. 101 runCatching { cache[uri].await() }.getOrNull() 102 103 @OptIn(ExperimentalCoroutinesApi::class) 104 override fun getCachedBitmap(uri: Uri): Bitmap? = 105 kotlin.runCatching { cache[uri].getCompleted() }.getOrNull() 106 107 companion object { 108 private const val TAG = "CachingImgPrevLoader" 109 } 110 } 111