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