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