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