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