• 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.widget
18 
19 import android.graphics.Bitmap
20 import android.net.Uri
21 import com.android.intentresolver.captureMany
22 import com.android.intentresolver.mock
23 import com.android.intentresolver.widget.ScrollableImagePreviewView.BatchPreviewLoader
24 import com.android.intentresolver.widget.ScrollableImagePreviewView.Preview
25 import com.android.intentresolver.widget.ScrollableImagePreviewView.PreviewType
26 import com.android.intentresolver.withArgCaptor
27 import com.google.common.truth.Truth.assertThat
28 import kotlinx.coroutines.CompletableDeferred
29 import kotlinx.coroutines.CoroutineScope
30 import kotlinx.coroutines.Dispatchers
31 import kotlinx.coroutines.ExperimentalCoroutinesApi
32 import kotlinx.coroutines.cancel
33 import kotlinx.coroutines.flow.MutableSharedFlow
34 import kotlinx.coroutines.flow.asFlow
35 import kotlinx.coroutines.launch
36 import kotlinx.coroutines.test.UnconfinedTestDispatcher
37 import kotlinx.coroutines.test.resetMain
38 import kotlinx.coroutines.test.setMain
39 import org.junit.After
40 import org.junit.Before
41 import org.junit.Test
42 import org.mockito.Mockito.atLeast
43 import org.mockito.Mockito.times
44 import org.mockito.Mockito.verify
45 
46 @OptIn(ExperimentalCoroutinesApi::class)
47 class BatchPreviewLoaderTest {
48     private val dispatcher = UnconfinedTestDispatcher()
49     private val testScope = CoroutineScope(dispatcher)
50     private val onCompletion = mock<() -> Unit>()
51     private val onUpdate = mock<(List<Preview>) -> Unit>()
52 
53     @Before
54     fun setup() {
55         Dispatchers.setMain(dispatcher)
56     }
57 
58     @After
59     fun cleanup() {
60         testScope.cancel()
61         Dispatchers.resetMain()
62     }
63 
64     @Test
65     fun test_allImagesWithinViewPort_oneUpdate() {
66         val imageLoader = TestImageLoader(testScope)
67         val uriOne = createUri(1)
68         val uriTwo = createUri(2)
69         imageLoader.setUriLoadingOrder(succeed(uriTwo), succeed(uriOne))
70         val testSubject =
71             BatchPreviewLoader(
72                 imageLoader,
73                 previews(uriOne, uriTwo),
74                 totalItemCount = 2,
75                 onUpdate,
76                 onCompletion
77             )
78         testSubject.loadAspectRatios(200) { _, _, _ -> 100 }
79         dispatcher.scheduler.advanceUntilIdle()
80 
81         verify(onCompletion, times(1)).invoke()
82         val list = withArgCaptor { verify(onUpdate, times(1)).invoke(capture()) }.map { it.uri }
83         assertThat(list).containsExactly(uriOne, uriTwo).inOrder()
84     }
85 
86     @Test
87     fun test_allImagesWithinViewPortOneFailed_failedPreviewIsNotUpdated() {
88         val imageLoader = TestImageLoader(testScope)
89         val uriOne = createUri(1)
90         val uriTwo = createUri(2)
91         val uriThree = createUri(3)
92         imageLoader.setUriLoadingOrder(succeed(uriThree), fail(uriTwo), succeed(uriOne))
93         val testSubject =
94             BatchPreviewLoader(
95                 imageLoader,
96                 previews(uriOne, uriTwo, uriThree),
97                 totalItemCount = 3,
98                 onUpdate,
99                 onCompletion
100             )
101         testSubject.loadAspectRatios(200) { _, _, _ -> 100 }
102         dispatcher.scheduler.advanceUntilIdle()
103 
104         verify(onCompletion, times(1)).invoke()
105         val list = withArgCaptor { verify(onUpdate, times(1)).invoke(capture()) }.map { it.uri }
106         assertThat(list).containsExactly(uriOne, uriThree).inOrder()
107     }
108 
109     @Test
110     fun test_imagesLoadedNotInOrder_updatedInOrder() {
111         val imageLoader = TestImageLoader(testScope)
112         val uris = Array(10) { createUri(it) }
113         val loadingOrder =
114             Array(uris.size) { i ->
115                 val uriIdx =
116                     when {
117                         i % 2 == 1 -> i - 1
118                         i % 2 == 0 && i < uris.size - 1 -> i + 1
119                         else -> i
120                     }
121                 succeed(uris[uriIdx])
122             }
123         imageLoader.setUriLoadingOrder(*loadingOrder)
124         val testSubject =
125             BatchPreviewLoader(imageLoader, previews(*uris), uris.size, onUpdate, onCompletion)
126         testSubject.loadAspectRatios(200) { _, _, _ -> 100 }
127         dispatcher.scheduler.advanceUntilIdle()
128 
129         verify(onCompletion, times(1)).invoke()
130         val list =
131             captureMany { verify(onUpdate, atLeast(1)).invoke(capture()) }
132                 .fold(ArrayList<Preview>()) { acc, update -> acc.apply { addAll(update) } }
133                 .map { it.uri }
134         assertThat(list).containsExactly(*uris).inOrder()
135     }
136 
137     @Test
138     fun test_imagesLoadedNotInOrderSomeFailed_updatedInOrder() {
139         val imageLoader = TestImageLoader(testScope)
140         val uris = Array(10) { createUri(it) }
141         val loadingOrder =
142             Array(uris.size) { i ->
143                 val uriIdx =
144                     when {
145                         i % 2 == 1 -> i - 1
146                         i % 2 == 0 && i < uris.size - 1 -> i + 1
147                         else -> i
148                     }
149                 if (uriIdx % 2 == 0) fail(uris[uriIdx]) else succeed(uris[uriIdx])
150             }
151         val expectedUris = Array(uris.size / 2) { createUri(it * 2 + 1) }
152         imageLoader.setUriLoadingOrder(*loadingOrder)
153         val testSubject =
154             BatchPreviewLoader(imageLoader, previews(*uris), uris.size, onUpdate, onCompletion)
155         testSubject.loadAspectRatios(200) { _, _, _ -> 100 }
156         dispatcher.scheduler.advanceUntilIdle()
157 
158         verify(onCompletion, times(1)).invoke()
159         val list =
160             captureMany { verify(onUpdate, atLeast(1)).invoke(capture()) }
161                 .fold(ArrayList<Preview>()) { acc, update -> acc.apply { addAll(update) } }
162                 .map { it.uri }
163         assertThat(list).containsExactly(*expectedUris).inOrder()
164     }
165 
166     private fun createUri(idx: Int): Uri = Uri.parse("content://org.pkg.app/image-$idx.png")
167 
168     private fun fail(uri: Uri) = uri to false
169     private fun succeed(uri: Uri) = uri to true
170     private fun previews(vararg uris: Uri) =
171         uris
172             .fold(ArrayList<Preview>(uris.size)) { acc, uri ->
173                 acc.apply { add(Preview(PreviewType.Image, uri, editAction = null)) }
174             }
175             .asFlow()
176 }
177 
178 private class TestImageLoader(scope: CoroutineScope) : suspend (Uri, Boolean) -> Bitmap? {
179     private val loadingOrder = ArrayDeque<Pair<Uri, Boolean>>()
180     private val pendingRequests = LinkedHashMap<Uri, CompletableDeferred<Bitmap?>>()
181     private val flow = MutableSharedFlow<Unit>(replay = 1)
<lambda>null182     private val bitmap by lazy { Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888) }
183 
<lambda>null184     init {
185         scope.launch {
186             flow.collect {
187                 while (true) {
188                     val (nextUri, isLoaded) = loadingOrder.firstOrNull() ?: break
189                     val deferred = pendingRequests.remove(nextUri) ?: break
190                     loadingOrder.removeFirst()
191                     deferred.complete(if (isLoaded) bitmap else null)
192                 }
193                 if (loadingOrder.isEmpty()) {
194                     pendingRequests.forEach { (uri, deferred) -> deferred.complete(bitmap) }
195                     pendingRequests.clear()
196                 }
197             }
198         }
199     }
200 
setUriLoadingOrdernull201     fun setUriLoadingOrder(vararg uris: Pair<Uri, Boolean>) {
202         loadingOrder.clear()
203         loadingOrder.addAll(uris)
204     }
205 
invokenull206     override suspend fun invoke(uri: Uri, cache: Boolean): Bitmap? {
207         val deferred = pendingRequests.getOrPut(uri) { CompletableDeferred() }
208         flow.tryEmit(Unit)
209         return deferred.await()
210     }
211 }
212