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