1 /* <lambda>null2 * Copyright (C) 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.intentresolver.contentpreview 18 19 import android.content.ContentResolver 20 import android.graphics.Bitmap 21 import android.net.Uri 22 import android.util.Log 23 import android.util.Size 24 import androidx.annotation.GuardedBy 25 import androidx.annotation.VisibleForTesting 26 import androidx.collection.LruCache 27 import androidx.lifecycle.Lifecycle 28 import androidx.lifecycle.coroutineScope 29 import java.util.function.Consumer 30 import kotlinx.coroutines.CancellationException 31 import kotlinx.coroutines.CompletableDeferred 32 import kotlinx.coroutines.CoroutineScope 33 import kotlinx.coroutines.Deferred 34 import kotlinx.coroutines.isActive 35 import kotlinx.coroutines.launch 36 import kotlinx.coroutines.sync.Semaphore 37 38 private const val TAG = "ImagePreviewImageLoader" 39 40 /** 41 * Implements preview image loading for the content preview UI. Provides requests deduplication, 42 * image caching, and a limit on the number of parallel loadings. 43 */ 44 @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) 45 class ImagePreviewImageLoader 46 @VisibleForTesting 47 constructor( 48 private val scope: CoroutineScope, 49 thumbnailSize: Int, 50 private val contentResolver: ContentResolver, 51 cacheSize: Int, 52 // TODO: consider providing a scope with the dispatcher configured with 53 // [CoroutineDispatcher#limitedParallelism] instead 54 private val contentResolverSemaphore: Semaphore, 55 ) : ImageLoader { 56 57 constructor( 58 scope: CoroutineScope, 59 thumbnailSize: Int, 60 contentResolver: ContentResolver, 61 cacheSize: Int, 62 maxSimultaneousRequests: Int = 4 63 ) : this(scope, thumbnailSize, contentResolver, cacheSize, Semaphore(maxSimultaneousRequests)) 64 65 private val thumbnailSize: Size = Size(thumbnailSize, thumbnailSize) 66 67 private val lock = Any() 68 @GuardedBy("lock") private val cache = LruCache<Uri, RequestRecord>(cacheSize) 69 @GuardedBy("lock") private val runningRequests = HashMap<Uri, RequestRecord>() 70 71 override suspend fun invoke(uri: Uri, caching: Boolean): Bitmap? = loadImageAsync(uri, caching) 72 73 override fun loadImage(callerLifecycle: Lifecycle, uri: Uri, callback: Consumer<Bitmap?>) { 74 callerLifecycle.coroutineScope.launch { 75 val image = loadImageAsync(uri, caching = true) 76 if (isActive) { 77 callback.accept(image) 78 } 79 } 80 } 81 82 override fun prePopulate(uris: List<Uri>) { 83 uris.asSequence().take(cache.maxSize()).forEach { uri -> 84 scope.launch { loadImageAsync(uri, caching = true) } 85 } 86 } 87 88 private suspend fun loadImageAsync(uri: Uri, caching: Boolean): Bitmap? { 89 return getRequestDeferred(uri, caching).await() 90 } 91 92 private fun getRequestDeferred(uri: Uri, caching: Boolean): Deferred<Bitmap?> { 93 var shouldLaunchImageLoading = false 94 val request = 95 synchronized(lock) { 96 cache[uri] 97 ?: runningRequests 98 .getOrPut(uri) { 99 shouldLaunchImageLoading = true 100 RequestRecord(uri, CompletableDeferred(), caching) 101 } 102 .apply { this.caching = this.caching || caching } 103 } 104 if (shouldLaunchImageLoading) { 105 request.loadBitmapAsync() 106 } 107 return request.deferred 108 } 109 110 private fun RequestRecord.loadBitmapAsync() { 111 scope 112 .launch { loadBitmap() } 113 .invokeOnCompletion { cause -> 114 if (cause is CancellationException) { 115 cancel() 116 } 117 } 118 } 119 120 private suspend fun RequestRecord.loadBitmap() { 121 contentResolverSemaphore.acquire() 122 val bitmap = 123 try { 124 contentResolver.loadThumbnail(uri, thumbnailSize, null) 125 } catch (t: Throwable) { 126 Log.d(TAG, "failed to load $uri preview", t) 127 null 128 } finally { 129 contentResolverSemaphore.release() 130 } 131 complete(bitmap) 132 } 133 134 private fun RequestRecord.cancel() { 135 synchronized(lock) { 136 runningRequests.remove(uri) 137 deferred.cancel() 138 } 139 } 140 141 private fun RequestRecord.complete(bitmap: Bitmap?) { 142 deferred.complete(bitmap) 143 synchronized(lock) { 144 runningRequests.remove(uri) 145 if (bitmap != null && caching) { 146 cache.put(uri, this) 147 } 148 } 149 } 150 151 private class RequestRecord( 152 val uri: Uri, 153 val deferred: CompletableDeferred<Bitmap?>, 154 @GuardedBy("lock") var caching: Boolean 155 ) 156 } 157