1 /*
2  * 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.contentpreview
18 
19 import android.content.ContentInterface
20 import android.content.Intent
21 import android.database.MatrixCursor
22 import android.media.MediaMetadata
23 import android.net.Uri
24 import android.platform.test.flag.junit.CheckFlagsRule
25 import android.platform.test.flag.junit.DeviceFlagsValueProvider
26 import android.provider.DocumentsContract
27 import com.google.common.truth.Truth.assertThat
28 import kotlin.coroutines.EmptyCoroutineContext
29 import kotlinx.coroutines.CoroutineScope
30 import kotlinx.coroutines.ExperimentalCoroutinesApi
31 import kotlinx.coroutines.flow.toList
32 import kotlinx.coroutines.test.TestScope
33 import kotlinx.coroutines.test.UnconfinedTestDispatcher
34 import kotlinx.coroutines.test.runTest
35 import org.junit.Rule
36 import org.junit.Test
37 import org.mockito.kotlin.any
38 import org.mockito.kotlin.mock
39 import org.mockito.kotlin.never
40 import org.mockito.kotlin.times
41 import org.mockito.kotlin.verify
42 import org.mockito.kotlin.whenever
43 
44 @OptIn(ExperimentalCoroutinesApi::class)
45 class PreviewDataProviderTest {
46     private val contentResolver = mock<ContentInterface>()
47     private val mimeTypeClassifier = DefaultMimeTypeClassifier
48     private val testScope = TestScope(EmptyCoroutineContext + UnconfinedTestDispatcher())
49     @get:Rule val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
50 
createDataProvidernull51     private fun createDataProvider(
52         targetIntent: Intent,
53         scope: CoroutineScope = testScope,
54         additionalContentUri: Uri? = null,
55         resolver: ContentInterface = contentResolver,
56         typeClassifier: MimeTypeClassifier = mimeTypeClassifier,
57         isPayloadTogglingEnabled: Boolean = false
58     ) =
59         PreviewDataProvider(
60             scope,
61             targetIntent,
62             additionalContentUri,
63             resolver,
64             isPayloadTogglingEnabled,
65             typeClassifier,
66         )
67 
68     @Test
69     fun test_nonSendIntentAction_resolvesToTextPreviewUiSynchronously() {
70         val targetIntent = Intent(Intent.ACTION_VIEW)
71         val testSubject = createDataProvider(targetIntent)
72 
73         assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_TEXT)
74         verify(contentResolver, never()).getType(any())
75     }
76 
77     @Test
test_sendSingleTextFileWithoutPreview_resolvesToFilePreviewUinull78     fun test_sendSingleTextFileWithoutPreview_resolvesToFilePreviewUi() {
79         val uri = Uri.parse("content://org.pkg.app/notes.txt")
80         val targetIntent =
81             Intent(Intent.ACTION_SEND).apply {
82                 putExtra(Intent.EXTRA_STREAM, uri)
83                 type = "text/plain"
84             }
85         whenever(contentResolver.getType(uri)).thenReturn("text/plain")
86         val testSubject = createDataProvider(targetIntent)
87 
88         assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE)
89         assertThat(testSubject.uriCount).isEqualTo(1)
90         assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri)
91         verify(contentResolver, times(1)).getType(any())
92     }
93 
94     @Test
test_sendIntentWithoutUris_resolvesToTextPreviewUiSynchronouslynull95     fun test_sendIntentWithoutUris_resolvesToTextPreviewUiSynchronously() {
96         val targetIntent = Intent(Intent.ACTION_SEND).apply { type = "image/png" }
97         val testSubject = createDataProvider(targetIntent)
98 
99         assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_TEXT)
100         verify(contentResolver, never()).getType(any())
101     }
102 
103     @Test
test_sendSingleImage_resolvesToImagePreviewUinull104     fun test_sendSingleImage_resolvesToImagePreviewUi() {
105         val uri = Uri.parse("content://org.pkg.app/image.png")
106         val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) }
107         whenever(contentResolver.getType(uri)).thenReturn("image/png")
108         val testSubject = createDataProvider(targetIntent)
109 
110         assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE)
111         assertThat(testSubject.uriCount).isEqualTo(1)
112         assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri)
113         assertThat(testSubject.firstFileInfo?.previewUri).isEqualTo(uri)
114         verify(contentResolver, times(1)).getType(any())
115     }
116 
117     @Test
test_sendSingleNonImage_resolvesToFilePreviewUinull118     fun test_sendSingleNonImage_resolvesToFilePreviewUi() {
119         val uri = Uri.parse("content://org.pkg.app/paper.pdf")
120         val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) }
121         whenever(contentResolver.getType(uri)).thenReturn("application/pdf")
122         val testSubject = createDataProvider(targetIntent)
123 
124         assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE)
125         assertThat(testSubject.uriCount).isEqualTo(1)
126         assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri)
127         assertThat(testSubject.firstFileInfo?.previewUri).isNull()
128         verify(contentResolver, times(1)).getType(any())
129     }
130 
131     @Test
test_sendSingleImageWithFailingGetType_resolvesToFilePreviewUinull132     fun test_sendSingleImageWithFailingGetType_resolvesToFilePreviewUi() {
133         val uri = Uri.parse("content://org.pkg.app/image.png")
134         val targetIntent =
135             Intent(Intent.ACTION_SEND).apply {
136                 type = "image/png"
137                 putExtra(Intent.EXTRA_STREAM, uri)
138             }
139         whenever(contentResolver.getType(uri)).thenThrow(SecurityException("test failure"))
140         val testSubject = createDataProvider(targetIntent)
141 
142         assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE)
143         assertThat(testSubject.uriCount).isEqualTo(1)
144         assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri)
145         assertThat(testSubject.firstFileInfo?.previewUri).isNull()
146         verify(contentResolver, times(1)).getType(any())
147     }
148 
149     @Test
test_sendSingleImageWithFailingMetadata_resolvesToFilePreviewUinull150     fun test_sendSingleImageWithFailingMetadata_resolvesToFilePreviewUi() {
151         val uri = Uri.parse("content://org.pkg.app/image.png")
152         val targetIntent =
153             Intent(Intent.ACTION_SEND).apply {
154                 type = "image/png"
155                 putExtra(Intent.EXTRA_STREAM, uri)
156             }
157         whenever(contentResolver.getStreamTypes(uri, "*/*"))
158             .thenThrow(SecurityException("test failure"))
159         whenever(contentResolver.query(uri, METADATA_COLUMNS, null, null))
160             .thenThrow(SecurityException("test failure"))
161         val testSubject = createDataProvider(targetIntent)
162 
163         assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE)
164         assertThat(testSubject.uriCount).isEqualTo(1)
165         assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri)
166         assertThat(testSubject.firstFileInfo?.previewUri).isNull()
167         verify(contentResolver, times(1)).getType(any())
168     }
169 
170     @Test
test_SingleNonImageUriWithImageTypeInGetStreamTypes_useImagePreviewUinull171     fun test_SingleNonImageUriWithImageTypeInGetStreamTypes_useImagePreviewUi() {
172         val uri = Uri.parse("content://org.pkg.app/paper.pdf")
173         val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) }
174         whenever(contentResolver.getStreamTypes(uri, "*/*"))
175             .thenReturn(arrayOf("application/pdf", "image/png"))
176         val testSubject = createDataProvider(targetIntent)
177 
178         assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE)
179         assertThat(testSubject.uriCount).isEqualTo(1)
180         assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri)
181         assertThat(testSubject.firstFileInfo?.previewUri).isEqualTo(uri)
182         verify(contentResolver, times(1)).getType(any())
183     }
184 
185     @Test
test_SingleNonImageUriWithThumbnailFlag_useImagePreviewUinull186     fun test_SingleNonImageUriWithThumbnailFlag_useImagePreviewUi() {
187         testMetadataToImagePreview(
188             columns = arrayOf(DocumentsContract.Document.COLUMN_FLAGS),
189             values =
190                 arrayOf(
191                     DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL or
192                         DocumentsContract.Document.FLAG_SUPPORTS_METADATA
193                 )
194         )
195     }
196 
197     @Test
test_SingleNonImageUriWithMetadataIconUri_useImagePreviewUinull198     fun test_SingleNonImageUriWithMetadataIconUri_useImagePreviewUi() {
199         testMetadataToImagePreview(
200             columns = arrayOf(MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI),
201             values = arrayOf("content://org.pkg.app/test.pdf?thumbnail"),
202         )
203     }
204 
testMetadataToImagePreviewnull205     private fun testMetadataToImagePreview(columns: Array<String>, values: Array<Any>) {
206         val uri = Uri.parse("content://org.pkg.app/test.pdf")
207         val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) }
208         whenever(contentResolver.getType(uri)).thenReturn("application/pdf")
209         val cursor = MatrixCursor(columns).apply { addRow(values) }
210         whenever(contentResolver.query(uri, METADATA_COLUMNS, null, null)).thenReturn(cursor)
211 
212         val testSubject = createDataProvider(targetIntent)
213 
214         assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE)
215         assertThat(testSubject.uriCount).isEqualTo(1)
216         assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri)
217         assertThat(testSubject.firstFileInfo?.previewUri).isNotNull()
218         verify(contentResolver, times(1)).getType(any())
219         assertThat(cursor.isClosed).isTrue()
220     }
221 
222     @Test
test_emptyQueryResult_cursorGetsClosednull223     fun test_emptyQueryResult_cursorGetsClosed() {
224         val uri = Uri.parse("content://org.pkg.app/test.pdf")
225         val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) }
226         whenever(contentResolver.getType(uri)).thenReturn("application/pdf")
227         val cursor = MatrixCursor(emptyArray())
228         whenever(contentResolver.query(uri, METADATA_COLUMNS, null, null)).thenReturn(cursor)
229 
230         val testSubject = createDataProvider(targetIntent)
231 
232         assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE)
233         verify(contentResolver, times(1)).query(uri, METADATA_COLUMNS, null, null)
234         assertThat(cursor.isClosed).isTrue()
235     }
236 
237     @Test
test_multipleImageUri_useImagePreviewUinull238     fun test_multipleImageUri_useImagePreviewUi() {
239         val uri1 = Uri.parse("content://org.pkg.app/test.png")
240         val uri2 = Uri.parse("content://org.pkg.app/test.jpg")
241         val targetIntent =
242             Intent(Intent.ACTION_SEND_MULTIPLE).apply {
243                 putExtra(
244                     Intent.EXTRA_STREAM,
245                     ArrayList<Uri>().apply {
246                         add(uri1)
247                         add(uri2)
248                     }
249                 )
250             }
251         whenever(contentResolver.getType(uri1)).thenReturn("image/png")
252         whenever(contentResolver.getType(uri2)).thenReturn("image/jpeg")
253         val testSubject = createDataProvider(targetIntent)
254 
255         assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE)
256         assertThat(testSubject.uriCount).isEqualTo(2)
257         assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri1)
258         assertThat(testSubject.firstFileInfo?.previewUri).isEqualTo(uri1)
259         // preview type can be determined by the first URI type
260         verify(contentResolver, times(1)).getType(any())
261     }
262 
263     @Test
test_SomeImageUri_useImagePreviewUinull264     fun test_SomeImageUri_useImagePreviewUi() {
265         val uri1 = Uri.parse("content://org.pkg.app/test.png")
266         val uri2 = Uri.parse("content://org.pkg.app/test.pdf")
267         whenever(contentResolver.getType(uri1)).thenReturn("image/png")
268         whenever(contentResolver.getType(uri2)).thenReturn("application/pdf")
269         val targetIntent =
270             Intent(Intent.ACTION_SEND_MULTIPLE).apply {
271                 putExtra(
272                     Intent.EXTRA_STREAM,
273                     ArrayList<Uri>().apply {
274                         add(uri1)
275                         add(uri2)
276                     }
277                 )
278             }
279         val testSubject = createDataProvider(targetIntent)
280 
281         assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE)
282         assertThat(testSubject.uriCount).isEqualTo(2)
283         assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri1)
284         assertThat(testSubject.firstFileInfo?.previewUri).isEqualTo(uri1)
285         // preview type can be determined by the first URI type
286         verify(contentResolver, times(1)).getType(any())
287     }
288 
289     @Test
test_someNonImageUriWithPreview_useImagePreviewUinull290     fun test_someNonImageUriWithPreview_useImagePreviewUi() {
291         val uri1 = Uri.parse("content://org.pkg.app/test.mp4")
292         val uri2 = Uri.parse("content://org.pkg.app/test.pdf")
293         val targetIntent =
294             Intent(Intent.ACTION_SEND_MULTIPLE).apply {
295                 putExtra(
296                     Intent.EXTRA_STREAM,
297                     ArrayList<Uri>().apply {
298                         add(uri1)
299                         add(uri2)
300                     }
301                 )
302             }
303         whenever(contentResolver.getType(uri1)).thenReturn("video/mpeg4")
304         whenever(contentResolver.getStreamTypes(uri1, "*/*")).thenReturn(arrayOf("image/png"))
305         whenever(contentResolver.getType(uri2)).thenReturn("application/pdf")
306         val testSubject = createDataProvider(targetIntent)
307 
308         assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE)
309         assertThat(testSubject.uriCount).isEqualTo(2)
310         assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri1)
311         assertThat(testSubject.firstFileInfo?.previewUri).isEqualTo(uri1)
312         verify(contentResolver, times(2)).getType(any())
313     }
314 
315     @Test
test_allNonImageUrisWithoutPreview_useFilePreviewUinull316     fun test_allNonImageUrisWithoutPreview_useFilePreviewUi() {
317         val uri1 = Uri.parse("content://org.pkg.app/test.html")
318         val uri2 = Uri.parse("content://org.pkg.app/test.pdf")
319         val targetIntent =
320             Intent(Intent.ACTION_SEND_MULTIPLE).apply {
321                 putExtra(
322                     Intent.EXTRA_STREAM,
323                     ArrayList<Uri>().apply {
324                         add(uri1)
325                         add(uri2)
326                     }
327                 )
328             }
329         whenever(contentResolver.getType(uri1)).thenReturn("text/html")
330         whenever(contentResolver.getType(uri2)).thenReturn("application/pdf")
331         val testSubject = createDataProvider(targetIntent)
332 
333         assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE)
334         assertThat(testSubject.uriCount).isEqualTo(2)
335         assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri1)
336         assertThat(testSubject.firstFileInfo?.previewUri).isNull()
337         verify(contentResolver, times(2)).getType(any())
338     }
339 
340     @Test
test_imagePreviewFileInfoFlow_dataLoadedOncenull341     fun test_imagePreviewFileInfoFlow_dataLoadedOnce() =
342         testScope.runTest {
343             val uri1 = Uri.parse("content://org.pkg.app/test.html")
344             val uri2 = Uri.parse("content://org.pkg.app/test.pdf")
345             val targetIntent =
346                 Intent(Intent.ACTION_SEND_MULTIPLE).apply {
347                     putExtra(
348                         Intent.EXTRA_STREAM,
349                         ArrayList<Uri>().apply {
350                             add(uri1)
351                             add(uri2)
352                         }
353                     )
354                 }
355             whenever(contentResolver.getType(uri1)).thenReturn("text/html")
356             whenever(contentResolver.getType(uri2)).thenReturn("application/pdf")
357             whenever(contentResolver.getStreamTypes(uri1, "*/*"))
358                 .thenReturn(arrayOf("text/html", "image/jpeg"))
359             whenever(contentResolver.getStreamTypes(uri2, "*/*"))
360                 .thenReturn(arrayOf("application/pdf", "image/png"))
361             val testSubject = createDataProvider(targetIntent)
362 
363             val fileInfoListOne = testSubject.imagePreviewFileInfoFlow.toList()
364             val fileInfoListTwo = testSubject.imagePreviewFileInfoFlow.toList()
365 
366             assertThat(fileInfoListOne).hasSize(2)
367             assertThat(fileInfoListOne).containsAtLeastElementsIn(fileInfoListTwo).inOrder()
368 
369             verify(contentResolver, times(1)).getType(uri1)
370             verify(contentResolver, times(1)).getStreamTypes(uri1, "*/*")
371             verify(contentResolver, times(1)).getType(uri2)
372             verify(contentResolver, times(1)).getStreamTypes(uri2, "*/*")
373         }
374 
375     @Test
sendItemsWithAdditionalContentUri_showPayloadTogglingUinull376     fun sendItemsWithAdditionalContentUri_showPayloadTogglingUi() {
377         val uri = Uri.parse("content://org.pkg.app/image.png")
378         val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) }
379         whenever(contentResolver.getType(uri)).thenReturn("image/png")
380         val testSubject =
381             createDataProvider(
382                 targetIntent,
383                 additionalContentUri = Uri.parse("content://org.pkg.app.extracontent"),
384                 isPayloadTogglingEnabled = true,
385             )
386 
387         assertThat(testSubject.previewType)
388             .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_PAYLOAD_SELECTION)
389         assertThat(testSubject.uriCount).isEqualTo(1)
390         assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri)
391         assertThat(testSubject.firstFileInfo?.previewUri).isEqualTo(uri)
392         verify(contentResolver, times(1)).getType(any())
393     }
394 
395     @Test
sendItemsWithAdditionalContentUri_showImagePreviewUinull396     fun sendItemsWithAdditionalContentUri_showImagePreviewUi() {
397         val uri = Uri.parse("content://org.pkg.app/image.png")
398         val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) }
399         whenever(contentResolver.getType(uri)).thenReturn("image/png")
400         val testSubject =
401             createDataProvider(
402                 targetIntent,
403                 additionalContentUri = Uri.parse("content://org.pkg.app.extracontent"),
404             )
405 
406         assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE)
407         assertThat(testSubject.uriCount).isEqualTo(1)
408         assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri)
409         assertThat(testSubject.firstFileInfo?.previewUri).isEqualTo(uri)
410         verify(contentResolver, times(1)).getType(any())
411     }
412 
413     @Test
sendItemsWithAdditionalContentUriWithSameAuthority_showImagePreviewUinull414     fun sendItemsWithAdditionalContentUriWithSameAuthority_showImagePreviewUi() {
415         val uri = Uri.parse("content://org.pkg.app/image.png")
416         val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) }
417         whenever(contentResolver.getType(uri)).thenReturn("image/png")
418         val testSubject =
419             createDataProvider(
420                 targetIntent,
421                 additionalContentUri = Uri.parse("content://org.pkg.app/extracontent"),
422                 isPayloadTogglingEnabled = true,
423             )
424 
425         assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE)
426         assertThat(testSubject.uriCount).isEqualTo(1)
427         assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri)
428         assertThat(testSubject.firstFileInfo?.previewUri).isEqualTo(uri)
429         verify(contentResolver, times(1)).getType(any())
430     }
431 
432     @Test
test_nonSendIntentActionWithAdditionalContentUri_resolvesToTextPreviewUiSynchronouslynull433     fun test_nonSendIntentActionWithAdditionalContentUri_resolvesToTextPreviewUiSynchronously() {
434         val targetIntent = Intent(Intent.ACTION_VIEW)
435         val testSubject =
436             createDataProvider(
437                 targetIntent,
438                 additionalContentUri = Uri.parse("content://org.pkg.app/extracontent")
439             )
440 
441         assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_TEXT)
442         verify(contentResolver, never()).getType(any())
443     }
444 }
445