1 /*
<lambda>null2 * Copyright (C) 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 @file:OptIn(ExperimentalCoroutinesApi::class)
18
19 package com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor
20
21 import android.database.MatrixCursor
22 import android.net.Uri
23 import android.provider.MediaStore.MediaColumns.HEIGHT
24 import android.provider.MediaStore.MediaColumns.WIDTH
25 import android.util.Size
26 import androidx.core.os.bundleOf
27 import com.android.intentresolver.contentpreview.FileInfo
28 import com.android.intentresolver.contentpreview.UriMetadataReader
29 import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.cursorPreviewsRepository
30 import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.previewSelectionsRepository
31 import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.TargetIntentModifier
32 import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.targetIntentModifier
33 import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.CursorRow
34 import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel
35 import com.android.intentresolver.contentpreview.readSize
36 import com.android.intentresolver.contentpreview.uriMetadataReader
37 import com.android.intentresolver.util.KosmosTestScope
38 import com.android.intentresolver.util.cursor.CursorView
39 import com.android.intentresolver.util.cursor.viewBy
40 import com.android.intentresolver.util.runTest
41 import com.android.systemui.kosmos.Kosmos
42 import com.google.common.truth.Truth.assertThat
43 import kotlinx.coroutines.ExperimentalCoroutinesApi
44 import kotlinx.coroutines.launch
45 import org.junit.Test
46
47 class CursorPreviewsInteractorTest {
48
49 private fun runTestWithDeps(
50 initialSelection: Iterable<Int> = (1..2),
51 focusedItemIndex: Int = initialSelection.count() / 2,
52 cursor: Iterable<Int> = (0 until 4),
53 cursorStartPosition: Int = cursor.count() / 2,
54 pageSize: Int = 16,
55 maxLoadedPages: Int = 3,
56 cursorSizes: Map<Int, Size> = emptyMap(),
57 metadatSizes: Map<Int, Size> = emptyMap(),
58 block: KosmosTestScope.(TestDeps) -> Unit,
59 ) {
60 val metadataUriToSize = metadatSizes.mapKeys { uri(it.key) }
61 with(Kosmos()) {
62 this.focusedItemIndex = focusedItemIndex
63 this.pageSize = pageSize
64 this.maxLoadedPages = maxLoadedPages
65 this.targetIntentModifier = TargetIntentModifier { error("unexpected invocation") }
66 uriMetadataReader =
67 object : UriMetadataReader {
68 override fun getMetadata(uri: Uri): FileInfo =
69 FileInfo.Builder(uri)
70 .withPreviewUri(uri)
71 .withMimeType("image/bitmap")
72 .build()
73
74 override fun readPreviewSize(uri: Uri): Size? = metadataUriToSize[uri]
75 }
76 runTest {
77 block(
78 TestDeps(
79 initialSelection,
80 cursor,
81 cursorStartPosition,
82 cursorSizes,
83 )
84 )
85 }
86 }
87 }
88
89 private class TestDeps(
90 initialSelectionRange: Iterable<Int>,
91 private val cursorRange: Iterable<Int>,
92 private val cursorStartPosition: Int,
93 private val cursorSizes: Map<Int, Size>,
94 ) {
95 val cursor: CursorView<CursorRow?> =
96 MatrixCursor(arrayOf("uri", WIDTH, HEIGHT))
97 .apply {
98 extras = bundleOf("position" to cursorStartPosition)
99 for (i in cursorRange) {
100 val size = cursorSizes[i]
101 addRow(
102 arrayOf(
103 uri(i).toString(),
104 size?.width?.toString(),
105 size?.height?.toString(),
106 )
107 )
108 }
109 }
110 .viewBy {
111 getString(0)?.let { uriStr ->
112 CursorRow(Uri.parse(uriStr), readSize(), position)
113 }
114 }
115 val initialPreviews: List<PreviewModel> =
116 initialSelectionRange.map { i ->
117 PreviewModel(uri = uri(i), mimeType = "image/bitmap", order = i)
118 }
119 }
120
121 @Test
122 fun initialCursorLoad() =
123 runTestWithDeps(
124 cursor = (0 until 10),
125 cursorStartPosition = 2,
126 cursorSizes = mapOf(0 to (200 x 100)),
127 metadatSizes = mapOf(0 to (300 x 100), 3 to (400 x 100)),
128 pageSize = 2,
129 maxLoadedPages = 3,
130 ) { deps ->
131 backgroundScope.launch {
132 cursorPreviewsInteractor.launch(deps.cursor, deps.initialPreviews)
133 }
134 runCurrent()
135
136 assertThat(cursorPreviewsRepository.previewsModel.value).isNotNull()
137 with(cursorPreviewsRepository.previewsModel.value!!) {
138 assertThat(previewModels)
139 .containsExactlyElementsIn(
140 List(6) {
141 PreviewModel(
142 uri = Uri.fromParts("scheme$it", "ssp$it", "fragment$it"),
143 mimeType = "image/bitmap",
144 aspectRatio =
145 when (it) {
146 0 -> 2f
147 3 -> 4f
148 else -> 1f
149 },
150 order = it,
151 )
152 }
153 )
154 .inOrder()
155 assertThat(startIdx).isEqualTo(0)
156 assertThat(loadMoreLeft).isNull()
157 assertThat(loadMoreRight).isNotNull()
158 assertThat(leftTriggerIndex).isEqualTo(2)
159 assertThat(rightTriggerIndex).isEqualTo(4)
160 }
161 }
162
163 @Test
164 fun loadMoreLeft_evictRight() =
165 runTestWithDeps(
166 initialSelection = listOf(24),
167 cursor = (0 until 48),
168 pageSize = 16,
169 maxLoadedPages = 1,
170 ) { deps ->
171 backgroundScope.launch {
172 cursorPreviewsInteractor.launch(deps.cursor, deps.initialPreviews)
173 }
174 runCurrent()
175
176 assertThat(cursorPreviewsRepository.previewsModel.value).isNotNull()
177 assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels).hasSize(16)
178 assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.first().uri)
179 .isEqualTo(Uri.fromParts("scheme16", "ssp16", "fragment16"))
180 assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.last().uri)
181 .isEqualTo(Uri.fromParts("scheme31", "ssp31", "fragment31"))
182 assertThat(cursorPreviewsRepository.previewsModel.value!!.loadMoreLeft).isNotNull()
183
184 cursorPreviewsRepository.previewsModel.value!!.loadMoreLeft!!.invoke()
185 runCurrent()
186
187 assertThat(cursorPreviewsRepository.previewsModel.value).isNotNull()
188 assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels).hasSize(16)
189 assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.first().uri)
190 .isEqualTo(Uri.fromParts("scheme0", "ssp0", "fragment0"))
191 assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.last().uri)
192 .isEqualTo(Uri.fromParts("scheme15", "ssp15", "fragment15"))
193 assertThat(cursorPreviewsRepository.previewsModel.value!!.loadMoreLeft).isNull()
194 }
195
196 @Test
197 fun loadMoreRight_evictLeft() =
198 runTestWithDeps(
199 initialSelection = listOf(24),
200 cursor = (0 until 48),
201 pageSize = 16,
202 maxLoadedPages = 1,
203 ) { deps ->
204 backgroundScope.launch {
205 cursorPreviewsInteractor.launch(deps.cursor, deps.initialPreviews)
206 }
207 runCurrent()
208
209 assertThat(cursorPreviewsRepository.previewsModel.value).isNotNull()
210 assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels).hasSize(16)
211 assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.first().uri)
212 .isEqualTo(Uri.fromParts("scheme16", "ssp16", "fragment16"))
213 assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.last().uri)
214 .isEqualTo(Uri.fromParts("scheme31", "ssp31", "fragment31"))
215 assertThat(cursorPreviewsRepository.previewsModel.value!!.loadMoreRight).isNotNull()
216
217 cursorPreviewsRepository.previewsModel.value!!.loadMoreRight!!.invoke()
218 runCurrent()
219
220 assertThat(cursorPreviewsRepository.previewsModel.value).isNotNull()
221 assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels).hasSize(16)
222 assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.first().uri)
223 .isEqualTo(Uri.fromParts("scheme32", "ssp32", "fragment32"))
224 assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.last().uri)
225 .isEqualTo(Uri.fromParts("scheme47", "ssp47", "fragment47"))
226 }
227
228 @Test
229 fun noMoreRight_appendUnclaimedFromInitialSelection() =
230 runTestWithDeps(
231 initialSelection = listOf(24, 50),
232 cursor = listOf(24),
233 pageSize = 16,
234 maxLoadedPages = 2,
235 ) { deps ->
236 backgroundScope.launch {
237 cursorPreviewsInteractor.launch(deps.cursor, deps.initialPreviews)
238 }
239 runCurrent()
240
241 assertThat(cursorPreviewsRepository.previewsModel.value).isNotNull()
242 assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels).hasSize(2)
243 assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.first().uri)
244 .isEqualTo(Uri.fromParts("scheme24", "ssp24", "fragment24"))
245 assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.last().uri)
246 .isEqualTo(Uri.fromParts("scheme50", "ssp50", "fragment50"))
247 assertThat(cursorPreviewsRepository.previewsModel.value!!.loadMoreRight).isNull()
248 }
249
250 @Test
251 fun noMoreLeft_appendUnclaimedFromInitialSelection() =
252 runTestWithDeps(
253 initialSelection = listOf(0, 24),
254 cursor = listOf(24),
255 pageSize = 16,
256 maxLoadedPages = 2,
257 ) { deps ->
258 backgroundScope.launch {
259 cursorPreviewsInteractor.launch(deps.cursor, deps.initialPreviews)
260 }
261 runCurrent()
262
263 assertThat(cursorPreviewsRepository.previewsModel.value).isNotNull()
264 assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels).hasSize(2)
265 assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.first().uri)
266 .isEqualTo(Uri.fromParts("scheme0", "ssp0", "fragment0"))
267 assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.last().uri)
268 .isEqualTo(Uri.fromParts("scheme24", "ssp24", "fragment24"))
269 assertThat(cursorPreviewsRepository.previewsModel.value!!.loadMoreLeft).isNull()
270 }
271
272 @Test
273 fun unclaimedRecordsGotUpdatedInSelectionInteractor() =
274 runTestWithDeps(
275 initialSelection = listOf(1),
276 focusedItemIndex = 0,
277 cursor = listOf(0, 1),
278 cursorStartPosition = 1,
279 ) { deps ->
280 previewSelectionsRepository.selections.value =
281 PreviewModel(
282 uri = uri(1),
283 mimeType = "image/png",
284 order = 0,
285 ).let { mapOf(it.uri to it) }
286 backgroundScope.launch {
287 cursorPreviewsInteractor.launch(deps.cursor, deps.initialPreviews)
288 }
289 runCurrent()
290
291 assertThat(previewSelectionsRepository.selections.value.values).containsExactly(
292 PreviewModel(
293 uri = uri(1),
294 mimeType = "image/bitmap",
295 order = 1,
296 )
297 )
298 }
299 }
300
urinull301 private fun uri(index: Int) = Uri.fromParts("scheme$index", "ssp$index", "fragment$index")
302
303 private infix fun Int.x(height: Int) = Size(this, height)
304