• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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