• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright 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.Size
22 import com.google.common.truth.Truth.assertThat
23 import java.util.concurrent.atomic.AtomicInteger
24 import kotlinx.coroutines.CancellationException
25 import kotlinx.coroutines.CompletableDeferred
26 import kotlinx.coroutines.CoroutineScope
27 import kotlinx.coroutines.CoroutineStart
28 import kotlinx.coroutines.ExperimentalCoroutinesApi
29 import kotlinx.coroutines.async
30 import kotlinx.coroutines.awaitCancellation
31 import kotlinx.coroutines.cancel
32 import kotlinx.coroutines.launch
33 import kotlinx.coroutines.test.StandardTestDispatcher
34 import kotlinx.coroutines.test.TestScope
35 import kotlinx.coroutines.test.runCurrent
36 import kotlinx.coroutines.test.runTest
37 import org.junit.Test
38 
39 @OptIn(ExperimentalCoroutinesApi::class)
40 class PreviewImageLoaderTest {
41     private val scope = TestScope()
42 
43     @Test
44     fun test_cachingImageRequest_imageCached() =
45         scope.runTest {
46             val uri = createUri(0)
47             val thumbnailLoader =
48                 FakeThumbnailLoader().apply {
49                     fakeInvoke[uri] = { size -> createBitmap(size.width, size.height) }
50                 }
51             val testSubject =
52                 PreviewImageLoader(
53                     backgroundScope,
54                     1,
55                     100,
56                     thumbnailLoader,
57                     StandardTestDispatcher(scope.testScheduler),
58                 )
59 
60             val b1 = testSubject.invoke(uri, Size(200, 100))
61             val b2 = testSubject.invoke(uri, Size(200, 100), caching = false)
62             assertThat(b1).isEqualTo(b2)
63             assertThat(thumbnailLoader.invokeCalls).hasSize(1)
64         }
65 
66     @Test
67     fun test_nonCachingImageRequest_imageNotCached() =
68         scope.runTest {
69             val uri = createUri(0)
70             val thumbnailLoader =
71                 FakeThumbnailLoader().apply {
72                     fakeInvoke[uri] = { size -> createBitmap(size.width, size.height) }
73                 }
74             val testSubject =
75                 PreviewImageLoader(
76                     backgroundScope,
77                     1,
78                     100,
79                     thumbnailLoader,
80                     StandardTestDispatcher(scope.testScheduler),
81                 )
82 
83             testSubject.invoke(uri, Size(200, 100), caching = false)
84             testSubject.invoke(uri, Size(200, 100), caching = false)
85             assertThat(thumbnailLoader.invokeCalls).hasSize(2)
86         }
87 
88     @Test
89     fun test_twoSimultaneousImageRequests_requestsDeduplicated() =
90         scope.runTest {
91             val uri = createUri(0)
92             val loadingStartedDeferred = CompletableDeferred<Unit>()
93             val bitmapDeferred = CompletableDeferred<Bitmap>()
94             val thumbnailLoader =
95                 FakeThumbnailLoader().apply {
96                     fakeInvoke[uri] = {
97                         loadingStartedDeferred.complete(Unit)
98                         bitmapDeferred.await()
99                     }
100                 }
101             val testSubject =
102                 PreviewImageLoader(
103                     backgroundScope,
104                     1,
105                     100,
106                     thumbnailLoader,
107                     StandardTestDispatcher(scope.testScheduler),
108                 )
109 
110             val b1Deferred = async { testSubject.invoke(uri, Size(200, 100), caching = false) }
111             loadingStartedDeferred.await()
112             val b2Deferred =
113                 async(start = CoroutineStart.UNDISPATCHED) {
114                     testSubject.invoke(uri, Size(200, 100), caching = true)
115                 }
116             bitmapDeferred.complete(createBitmap(200, 200))
117 
118             val b1 = b1Deferred.await()
119             val b2 = b2Deferred.await()
120             assertThat(b1).isEqualTo(b2)
121             assertThat(thumbnailLoader.invokeCalls).hasSize(1)
122         }
123 
124     @Test
125     fun test_cachingRequestCancelledAndEvoked_imageLoadingCancelled() =
126         scope.runTest {
127             val uriOne = createUri(1)
128             val uriTwo = createUri(2)
129             val loadingStartedDeferred = CompletableDeferred<Unit>()
130             val cancelledRequests = mutableSetOf<Uri>()
131             val thumbnailLoader =
132                 FakeThumbnailLoader().apply {
133                     fakeInvoke[uriOne] = {
134                         loadingStartedDeferred.complete(Unit)
135                         try {
136                             awaitCancellation()
137                         } catch (e: CancellationException) {
138                             cancelledRequests.add(uriOne)
139                             throw e
140                         }
141                     }
142                     fakeInvoke[uriTwo] = { createBitmap(200, 200) }
143                 }
144             val testSubject =
145                 PreviewImageLoader(
146                     backgroundScope,
147                     cacheSize = 1,
148                     defaultPreviewSize = 100,
149                     thumbnailLoader,
150                     StandardTestDispatcher(scope.testScheduler),
151                 )
152 
153             val jobOne = launch { testSubject.invoke(uriOne, Size(200, 100)) }
154             loadingStartedDeferred.await()
155             jobOne.cancel()
156             scope.runCurrent()
157 
158             assertThat(cancelledRequests).isEmpty()
159 
160             // second URI should evict the first item from the cache
161             testSubject.invoke(uriTwo, Size(200, 100))
162 
163             assertThat(thumbnailLoader.invokeCalls).hasSize(2)
164             assertThat(cancelledRequests).containsExactly(uriOne)
165         }
166 
167     @Test
168     fun test_nonCachingRequestClientCancels_imageLoadingCancelled() =
169         scope.runTest {
170             val uri = createUri(1)
171             val loadingStartedDeferred = CompletableDeferred<Unit>()
172             val cancelledRequests = mutableSetOf<Uri>()
173             val thumbnailLoader =
174                 FakeThumbnailLoader().apply {
175                     fakeInvoke[uri] = {
176                         loadingStartedDeferred.complete(Unit)
177                         try {
178                             awaitCancellation()
179                         } catch (e: CancellationException) {
180                             cancelledRequests.add(uri)
181                             throw e
182                         }
183                     }
184                 }
185             val testSubject =
186                 PreviewImageLoader(
187                     backgroundScope,
188                     cacheSize = 1,
189                     defaultPreviewSize = 100,
190                     thumbnailLoader,
191                     StandardTestDispatcher(scope.testScheduler),
192                 )
193 
194             val job = launch { testSubject.invoke(uri, Size(200, 100), caching = false) }
195             loadingStartedDeferred.await()
196             job.cancel()
197             scope.runCurrent()
198 
199             assertThat(cancelledRequests).containsExactly(uri)
200         }
201 
202     @Test
203     fun test_requestHigherResImage_newImageLoaded() =
204         scope.runTest {
205             val uri = createUri(0)
206             val thumbnailLoader =
207                 FakeThumbnailLoader().apply {
208                     fakeInvoke[uri] = { size -> createBitmap(size.width, size.height) }
209                 }
210             val testSubject =
211                 PreviewImageLoader(
212                     backgroundScope,
213                     1,
214                     100,
215                     thumbnailLoader,
216                     StandardTestDispatcher(scope.testScheduler),
217                 )
218 
219             val b1 = testSubject.invoke(uri, Size(100, 100))
220             val b2 = testSubject.invoke(uri, Size(200, 200))
221             assertThat(b1).isNotNull()
222             assertThat(b1!!.width).isEqualTo(100)
223             assertThat(b2).isNotNull()
224             assertThat(b2!!.width).isEqualTo(200)
225             assertThat(thumbnailLoader.invokeCalls).hasSize(2)
226         }
227 
228     @Test
229     fun test_imageLoadingThrowsException_returnsNull() =
230         scope.runTest {
231             val uri = createUri(0)
232             val thumbnailLoader =
233                 FakeThumbnailLoader().apply {
234                     fakeInvoke[uri] = { throw SecurityException("test") }
235                 }
236             val testSubject =
237                 PreviewImageLoader(
238                     backgroundScope,
239                     1,
240                     100,
241                     thumbnailLoader,
242                     StandardTestDispatcher(scope.testScheduler),
243                 )
244 
245             val bitmap = testSubject.invoke(uri, Size(100, 100))
246             assertThat(bitmap).isNull()
247         }
248 
249     @Test
250     fun test_requestHigherResImage_cancelsLowerResLoading() =
251         scope.runTest {
252             val uri = createUri(0)
253             val cancelledRequestCount = AtomicInteger(0)
254             val imageLoadingStarted = CompletableDeferred<Unit>()
255             val bitmapDeferred = CompletableDeferred<Bitmap>()
256             val thumbnailLoader =
257                 FakeThumbnailLoader().apply {
258                     fakeInvoke[uri] = {
259                         imageLoadingStarted.complete(Unit)
260                         try {
261                             bitmapDeferred.await()
262                         } catch (e: CancellationException) {
263                             cancelledRequestCount.getAndIncrement()
264                             throw e
265                         }
266                     }
267                 }
268             val testSubject =
269                 PreviewImageLoader(
270                     backgroundScope,
271                     1,
272                     100,
273                     thumbnailLoader,
274                     StandardTestDispatcher(scope.testScheduler),
275                 )
276 
277             val lowResSize = 100
278             val highResSize = 200
279             launch(start = CoroutineStart.UNDISPATCHED) {
280                 testSubject.invoke(uri, Size(lowResSize, lowResSize))
281             }
282             imageLoadingStarted.await()
283             val result = async { testSubject.invoke(uri, Size(highResSize, highResSize)) }
284             runCurrent()
285             assertThat(cancelledRequestCount.get()).isEqualTo(1)
286 
287             bitmapDeferred.complete(createBitmap(highResSize, highResSize))
288             val bitmap = result.await()
289             assertThat(bitmap).isNotNull()
290             assertThat(bitmap!!.width).isEqualTo(highResSize)
291             assertThat(thumbnailLoader.invokeCalls).hasSize(2)
292         }
293 
294     @Test
295     fun test_requestLowerResImage_cachedHigherResImageReturned() =
296         scope.runTest {
297             val uri = createUri(0)
298             val thumbnailLoader =
299                 FakeThumbnailLoader().apply {
300                     fakeInvoke[uri] = { size -> createBitmap(size.width, size.height) }
301                 }
302             val lowResSize = 100
303             val highResSize = 200
304             val testSubject =
305                 PreviewImageLoader(
306                     backgroundScope,
307                     1,
308                     100,
309                     thumbnailLoader,
310                     StandardTestDispatcher(scope.testScheduler),
311                 )
312 
313             val b1 = testSubject.invoke(uri, Size(highResSize, highResSize))
314             val b2 = testSubject.invoke(uri, Size(lowResSize, lowResSize))
315             assertThat(b1).isEqualTo(b2)
316             assertThat(b2!!.width).isEqualTo(highResSize)
317             assertThat(thumbnailLoader.invokeCalls).hasSize(1)
318         }
319 
320     @Test
321     fun test_incorrectSizeRequested_defaultSizeIsUsed() =
322         scope.runTest {
323             val uri = createUri(0)
324             val defaultPreviewSize = 100
325             val thumbnailLoader =
326                 FakeThumbnailLoader().apply {
327                     fakeInvoke[uri] = { size -> createBitmap(size.width, size.height) }
328                 }
329             val testSubject =
330                 PreviewImageLoader(
331                     backgroundScope,
332                     cacheSize = 1,
333                     defaultPreviewSize,
334                     thumbnailLoader,
335                     StandardTestDispatcher(scope.testScheduler),
336                 )
337 
338             val b1 = testSubject(uri, Size(0, 0))
339             assertThat(b1!!.width).isEqualTo(defaultPreviewSize)
340 
341             val largerImageSize = 200
342             val b2 = testSubject(uri, Size(largerImageSize, largerImageSize))
343             assertThat(b2!!.width).isEqualTo(largerImageSize)
344         }
345 
346     @Test
347     fun test_prePopulateImages_cachesImagesUpToTheCacheSize() =
348         scope.runTest {
349             val previewSize = Size(100, 100)
350             val uris = List(2) { createUri(it) }
351             val loadingCount = AtomicInteger(0)
352             val thumbnailLoader =
353                 FakeThumbnailLoader().apply {
354                     for (uri in uris) {
355                         fakeInvoke[uri] = { size ->
356                             loadingCount.getAndIncrement()
357                             createBitmap(size.width, size.height)
358                         }
359                     }
360                 }
361             val testSubject =
362                 PreviewImageLoader(
363                     backgroundScope,
364                     1,
365                     100,
366                     thumbnailLoader,
367                     StandardTestDispatcher(scope.testScheduler),
368                 )
369 
370             testSubject.prePopulate(uris.map { it to previewSize })
371             runCurrent()
372 
373             assertThat(loadingCount.get()).isEqualTo(1)
374             assertThat(thumbnailLoader.invokeCalls).containsExactly(uris[0])
375 
376             testSubject(uris[0], previewSize)
377             runCurrent()
378 
379             assertThat(loadingCount.get()).isEqualTo(1)
380         }
381 
382     @Test
383     fun test_oldRecordEvictedFromTheCache() =
384         scope.runTest {
385             val previewSize = Size(100, 100)
386             val uriOne = createUri(1)
387             val uriTwo = createUri(2)
388             val requestsPerUri = HashMap<Uri, AtomicInteger>()
389             val thumbnailLoader =
390                 FakeThumbnailLoader().apply {
391                     for (uri in arrayOf(uriOne, uriTwo)) {
392                         fakeInvoke[uri] = { size ->
393                             requestsPerUri.getOrPut(uri) { AtomicInteger() }.incrementAndGet()
394                             createBitmap(size.width, size.height)
395                         }
396                     }
397                 }
398             val testSubject =
399                 PreviewImageLoader(
400                     backgroundScope,
401                     1,
402                     100,
403                     thumbnailLoader,
404                     StandardTestDispatcher(scope.testScheduler),
405                 )
406 
407             testSubject(uriOne, previewSize)
408             testSubject(uriTwo, previewSize)
409             testSubject(uriTwo, previewSize)
410             testSubject(uriOne, previewSize)
411 
412             assertThat(requestsPerUri[uriOne]?.get()).isEqualTo(2)
413             assertThat(requestsPerUri[uriTwo]?.get()).isEqualTo(1)
414         }
415 
416     @Test
417     fun test_doNotCacheNulls() =
418         scope.runTest {
419             val previewSize = Size(100, 100)
420             val uri = createUri(1)
421             val loadingCount = AtomicInteger(0)
422             val thumbnailLoader =
423                 FakeThumbnailLoader().apply {
424                     fakeInvoke[uri] = {
425                         loadingCount.getAndIncrement()
426                         null
427                     }
428                 }
429             val testSubject =
430                 PreviewImageLoader(
431                     backgroundScope,
432                     1,
433                     100,
434                     thumbnailLoader,
435                     StandardTestDispatcher(scope.testScheduler),
436                 )
437 
438             testSubject(uri, previewSize)
439             testSubject(uri, previewSize)
440 
441             assertThat(loadingCount.get()).isEqualTo(2)
442         }
443 
444     @Test(expected = CancellationException::class)
445     fun invoke_onClosedImageLoaderScope_throwsCancellationException() =
446         scope.runTest {
447             val uri = createUri(1)
448             val thumbnailLoader = FakeThumbnailLoader().apply { fakeInvoke[uri] = { null } }
449             val imageLoaderScope = CoroutineScope(coroutineContext)
450             val testSubject =
451                 PreviewImageLoader(
452                     imageLoaderScope,
453                     1,
454                     100,
455                     thumbnailLoader,
456                     StandardTestDispatcher(scope.testScheduler),
457                 )
458             imageLoaderScope.cancel()
459             testSubject(uri, Size(200, 200))
460         }
461 
462     @Test(expected = CancellationException::class)
463     fun invoke_imageLoaderScopeClosedMidflight_throwsCancellationException() =
464         scope.runTest {
465             val uri = createUri(1)
466             val loadingStarted = CompletableDeferred<Unit>()
467             val bitmapDeferred = CompletableDeferred<Bitmap?>()
468             val thumbnailLoader =
469                 FakeThumbnailLoader().apply {
470                     fakeInvoke[uri] = {
471                         loadingStarted.complete(Unit)
472                         bitmapDeferred.await()
473                     }
474                 }
475             val imageLoaderScope = CoroutineScope(coroutineContext)
476             val testSubject =
477                 PreviewImageLoader(
478                     imageLoaderScope,
479                     1,
480                     100,
481                     thumbnailLoader,
482                     StandardTestDispatcher(scope.testScheduler),
483                 )
484 
485             launch {
486                 loadingStarted.await()
487                 imageLoaderScope.cancel()
488             }
489             testSubject(uri, Size(200, 200))
490         }
491 }
492 
createUrinull493 private fun createUri(id: Int) = Uri.parse("content://org.pkg.app/image-$id.png")
494 
495 private fun createBitmap(width: Int, height: Int) =
496     Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
497