diff options
5 files changed, 313 insertions, 162 deletions
diff --git a/aconfig/FeatureFlags.aconfig b/aconfig/FeatureFlags.aconfig index c8ad2126..6ac6efb3 100644 --- a/aconfig/FeatureFlags.aconfig +++ b/aconfig/FeatureFlags.aconfig @@ -20,6 +20,16 @@ flag { } flag { + name: "individual_metadata_title_read" + namespace: "intentresolver" + description: "Enables separate title URI metadata calls" + bug: "304686417" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "refine_system_actions" namespace: "intentresolver" description: "This flag enables sending system actions to the caller refinement flow" diff --git a/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt b/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt index 9b2dbebf..07cbaa04 100644 --- a/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt +++ b/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt @@ -24,15 +24,16 @@ import android.provider.DocumentsContract import android.provider.DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL import android.provider.Downloads import android.provider.OpenableColumns +import android.service.chooser.Flags.chooserPayloadToggling import android.text.TextUtils import android.util.Log import androidx.annotation.OpenForTesting import androidx.annotation.VisibleForTesting +import com.android.intentresolver.Flags.individualMetadataTitleRead import com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_FILE import com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_IMAGE import com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_PAYLOAD_SELECTION import com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_TEXT -import com.android.intentresolver.inject.ChooserServiceFlags import com.android.intentresolver.measurements.runTracing import com.android.intentresolver.util.ownedByCurrentUser import java.util.concurrent.atomic.AtomicInteger @@ -55,14 +56,19 @@ import kotlinx.coroutines.withTimeoutOrNull * A set of metadata columns we read for a content URI (see * [PreviewDataProvider.UriRecord.readQueryResult] method). */ -@VisibleForTesting -val METADATA_COLUMNS = +private val METADATA_COLUMNS = arrayOf( DocumentsContract.Document.COLUMN_FLAGS, MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI, OpenableColumns.DISPLAY_NAME, - Downloads.Impl.COLUMN_TITLE + Downloads.Impl.COLUMN_TITLE, ) + +/** Preview-related metadata columns. */ +@VisibleForTesting +val ICON_METADATA_COLUMNS = + arrayOf(DocumentsContract.Document.COLUMN_FLAGS, MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI) + private const val TIMEOUT_MS = 1_000L /** @@ -77,7 +83,6 @@ constructor( private val targetIntent: Intent, private val additionalContentUri: Uri?, private val contentResolver: ContentInterface, - private val featureFlags: ChooserServiceFlags, private val typeClassifier: MimeTypeClassifier = DefaultMimeTypeClassifier, ) { @@ -128,7 +133,7 @@ constructor( * IMAGE, FILE, TEXT. */ if (!targetIntent.isSend || records.isEmpty()) { CONTENT_PREVIEW_TEXT - } else if (featureFlags.chooserPayloadToggling() && shouldShowPayloadSelection()) { + } else if (chooserPayloadToggling() && shouldShowPayloadSelection()) { // TODO: replace with the proper flags injection CONTENT_PREVIEW_PAYLOAD_SELECTION } else { @@ -141,7 +146,7 @@ constructor( Log.w( ContentPreviewUi.TAG, "An attempt to read preview type from a cancelled scope", - e + e, ) CONTENT_PREVIEW_FILE } @@ -159,7 +164,7 @@ constructor( Log.w( ContentPreviewUi.TAG, "Failed to check URI authorities; no payload toggling", - it + it, ) } .getOrDefault(false) @@ -183,7 +188,7 @@ constructor( Log.w( ContentPreviewUi.TAG, "An attempt to read first file info from a cancelled scope", - e + e, ) } builder.build() @@ -212,14 +217,20 @@ constructor( if (records.isEmpty()) { throw IndexOutOfBoundsException("There are no shared URIs") } - callerScope.launch { - val result = scope.async { getFirstFileName() }.await() - callback.accept(result) - } + callerScope.launch { callback.accept(getFirstFileName()) } } + /** + * Returns a title for the first shared URI which is read from URI metadata or, if the metadata + * is not provided, derived from the URI. + */ @Throws(IndexOutOfBoundsException::class) - private fun getFirstFileName(): String { + suspend fun getFirstFileName(): String { + return scope.async { getFirstFileNameInternal() }.await() + } + + @Throws(IndexOutOfBoundsException::class) + private fun getFirstFileNameInternal(): String { if (records.isEmpty()) throw IndexOutOfBoundsException("There are no shared URIs") val record = records[0] @@ -282,16 +293,23 @@ constructor( get() = query.supportsThumbnail val title: String - get() = query.title + get() = if (individualMetadataTitleRead()) titleFromQuery else query.title val iconUri: Uri? get() = query.iconUri - private val query by lazy { readQueryResult() } + private val query by lazy { + readQueryResult( + if (individualMetadataTitleRead()) ICON_METADATA_COLUMNS else METADATA_COLUMNS + ) + } + + private val titleFromQuery by lazy { + readDisplayNameFromQuery().takeIf { !TextUtils.isEmpty(it) } ?: readTitleFromQuery() + } - private fun readQueryResult(): QueryResult = - // TODO: rewrite using methods from UiMetadataHelpers.kt - contentResolver.querySafe(uri, METADATA_COLUMNS)?.use { cursor -> + private fun readQueryResult(columns: Array<String>): QueryResult = + contentResolver.querySafe(uri, columns)?.use { cursor -> if (!cursor.moveToFirst()) return@use null var flagColIdx = -1 @@ -329,12 +347,23 @@ constructor( QueryResult(supportsThumbnail, title, iconUri) } ?: QueryResult() + + private fun readTitleFromQuery(): String = readStringColumn(Downloads.Impl.COLUMN_TITLE) + + private fun readDisplayNameFromQuery(): String = + readStringColumn(OpenableColumns.DISPLAY_NAME) + + private fun readStringColumn(column: String): String = + contentResolver.querySafe(uri, arrayOf(column))?.use { cursor -> + if (!cursor.moveToFirst()) return@use null + cursor.readString(column) + } ?: "" } private class QueryResult( val supportsThumbnail: Boolean = false, val title: String = "", - val iconUri: Uri? = null + val iconUri: Uri? = null, ) } diff --git a/java/src/com/android/intentresolver/contentpreview/UriMetadataHelpers.kt b/java/src/com/android/intentresolver/contentpreview/UriMetadataHelpers.kt index c532b9a5..80d0e058 100644 --- a/java/src/com/android/intentresolver/contentpreview/UriMetadataHelpers.kt +++ b/java/src/com/android/intentresolver/contentpreview/UriMetadataHelpers.kt @@ -22,11 +22,8 @@ import android.media.MediaMetadata import android.net.Uri import android.provider.DocumentsContract import android.provider.DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL -import android.provider.Downloads import android.provider.MediaStore.MediaColumns.HEIGHT import android.provider.MediaStore.MediaColumns.WIDTH -import android.provider.OpenableColumns -import android.text.TextUtils import android.util.Log import android.util.Size import com.android.intentresolver.measurements.runTracing @@ -78,12 +75,7 @@ internal fun Cursor.readSupportsThumbnail(): Boolean = .getOrDefault(false) internal fun Cursor.readPreviewUri(): Uri? = - runCatching { - columnNames - .indexOf(MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI) - .takeIf { it >= 0 } - ?.let { getString(it)?.let(Uri::parse) } - } + runCatching { readString(MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI)?.let(Uri::parse) } .getOrNull() fun Cursor.readSize(): Size? { @@ -105,34 +97,15 @@ fun Cursor.readSize(): Size? { } } -internal fun Cursor.readTitle(): String = - runCatching { - var nameColIndex = -1 - var titleColIndex = -1 - // TODO: double-check why Cursor#getColumnInded didn't work - columnNames.forEachIndexed { i, columnName -> - when (columnName) { - OpenableColumns.DISPLAY_NAME -> nameColIndex = i - Downloads.Impl.COLUMN_TITLE -> titleColIndex = i - } - } - - var title = "" - if (nameColIndex >= 0) { - title = getString(nameColIndex) ?: "" - } - if (TextUtils.isEmpty(title) && titleColIndex >= 0) { - title = getString(titleColIndex) ?: "" - } - title - } - .getOrDefault("") +internal fun Cursor.readString(columnName: String): String? = + runCatching { columnNames.indexOf(columnName).takeIf { it >= 0 }?.let { getString(it) } } + .getOrNull() private fun logProviderPermissionWarning(uri: Uri, dataName: String) { // The ContentResolver already logs the exception. Log something more informative. Log.w( ContentPreviewUi.TAG, "Could not read $uri $dataName. If a preview is desired, call Intent#setClipData() to" + - " ensure that the sharesheet is given permission." + " ensure that the sharesheet is given permission.", ) } diff --git a/java/src/com/android/intentresolver/ui/viewmodel/ChooserViewModel.kt b/java/src/com/android/intentresolver/ui/viewmodel/ChooserViewModel.kt index e6f12750..fe7e9109 100644 --- a/java/src/com/android/intentresolver/ui/viewmodel/ChooserViewModel.kt +++ b/java/src/com/android/intentresolver/ui/viewmodel/ChooserViewModel.kt @@ -95,7 +95,6 @@ constructor( chooserRequest.targetIntent, chooserRequest.additionalContentUri, contentResolver, - flags, ) } diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/PreviewDataProviderTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/PreviewDataProviderTest.kt index 370ee044..3dae760c 100644 --- a/tests/unit/src/com/android/intentresolver/contentpreview/PreviewDataProviderTest.kt +++ b/tests/unit/src/com/android/intentresolver/contentpreview/PreviewDataProviderTest.kt @@ -21,9 +21,15 @@ import android.content.Intent import android.database.MatrixCursor import android.media.MediaMetadata import android.net.Uri +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.FlagsParameterization +import android.platform.test.flag.junit.SetFlagsRule import android.provider.DocumentsContract -import android.service.chooser.FakeFeatureFlagsImpl -import android.service.chooser.Flags +import android.provider.Downloads +import android.provider.OpenableColumns +import android.service.chooser.Flags.FLAG_CHOOSER_PAYLOAD_TOGGLING +import com.android.intentresolver.Flags.FLAG_INDIVIDUAL_METADATA_TITLE_READ import com.google.common.truth.Truth.assertThat import kotlin.coroutines.EmptyCoroutineContext import kotlinx.coroutines.CoroutineScope @@ -32,21 +38,26 @@ import kotlinx.coroutines.flow.toList import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever +@RunWith(Parameterized::class) @OptIn(ExperimentalCoroutinesApi::class) -class PreviewDataProviderTest { +class PreviewDataProviderTest(flags: FlagsParameterization) { private val contentResolver = mock<ContentInterface>() private val mimeTypeClassifier = DefaultMimeTypeClassifier private val testScope = TestScope(EmptyCoroutineContext + UnconfinedTestDispatcher()) - private val featureFlags = - FakeFeatureFlagsImpl().apply { setFlag(Flags.FLAG_CHOOSER_PAYLOAD_TOGGLING, false) } + @get:Rule val setFlagsRule = SetFlagsRule(flags) private fun createDataProvider( targetIntent: Intent, @@ -54,15 +65,7 @@ class PreviewDataProviderTest { additionalContentUri: Uri? = null, resolver: ContentInterface = contentResolver, typeClassifier: MimeTypeClassifier = mimeTypeClassifier, - ) = - PreviewDataProvider( - scope, - targetIntent, - additionalContentUri, - resolver, - featureFlags, - typeClassifier, - ) + ) = PreviewDataProvider(scope, targetIntent, additionalContentUri, resolver, typeClassifier) @Test fun test_nonSendIntentAction_resolvesToTextPreviewUiSynchronously() { @@ -74,21 +77,49 @@ class PreviewDataProviderTest { } @Test - fun test_sendSingleTextFileWithoutPreview_resolvesToFilePreviewUi() { - val uri = Uri.parse("content://org.pkg.app/notes.txt") - val targetIntent = - Intent(Intent.ACTION_SEND).apply { - putExtra(Intent.EXTRA_STREAM, uri) - type = "text/plain" - } - whenever(contentResolver.getType(uri)).thenReturn("text/plain") - val testSubject = createDataProvider(targetIntent) + fun test_sendSingleTextFileWithoutPreview_resolvesToFilePreviewUi() = + testScope.runTest { + val fileName = "notes.txt" + val uri = Uri.parse("content://org.pkg.app/$fileName") + val targetIntent = + Intent(Intent.ACTION_SEND).apply { + putExtra(Intent.EXTRA_STREAM, uri) + type = "text/plain" + } + whenever(contentResolver.getType(uri)).thenReturn("text/plain") + val testSubject = createDataProvider(targetIntent) - assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) - assertThat(testSubject.uriCount).isEqualTo(1) - assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri) - verify(contentResolver, times(1)).getType(any()) - } + assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) + assertThat(testSubject.uriCount).isEqualTo(1) + assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri) + assertThat(testSubject.getFirstFileName()).isEqualTo(fileName) + verify(contentResolver, times(1)).getType(any()) + } + + @Test + fun test_sendSingleTextFileWithDisplayNameAndTitle_displayNameTakesPrecedenceOverTitle() = + testScope.runTest { + val uri = Uri.parse("content://org.pkg.app/1234") + val targetIntent = + Intent(Intent.ACTION_SEND).apply { + putExtra(Intent.EXTRA_STREAM, uri) + type = "text/plain" + } + whenever(contentResolver.getType(uri)).thenReturn("text/plain") + val title = "Notes" + val displayName = "Notes.txt" + whenever(contentResolver.query(eq(uri), anyOrNull(), anyOrNull(), anyOrNull())) + .thenReturn( + MatrixCursor(arrayOf(Downloads.Impl.COLUMN_TITLE, OpenableColumns.DISPLAY_NAME)) + .apply { addRow(arrayOf(title, displayName)) } + ) + contentResolver.setTitle(uri, title) + contentResolver.setDisplayName(uri, displayName) + val testSubject = createDataProvider(targetIntent) + + assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) + assertThat(testSubject.getFirstFileName()).isEqualTo(displayName) + } @Test fun test_sendIntentWithoutUris_resolvesToTextPreviewUiSynchronously() { @@ -114,60 +145,145 @@ class PreviewDataProviderTest { } @Test - fun test_sendSingleNonImage_resolvesToFilePreviewUi() { - val uri = Uri.parse("content://org.pkg.app/paper.pdf") - val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) } - whenever(contentResolver.getType(uri)).thenReturn("application/pdf") - val testSubject = createDataProvider(targetIntent) + fun test_sendSingleFile_resolvesToFilePreviewUi() = + testScope.runTest { + val fileName = "paper.pdf" + val uri = Uri.parse("content://org.pkg.app/$fileName") + val targetIntent = + Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) } + whenever(contentResolver.getType(uri)).thenReturn("application/pdf") + val testSubject = createDataProvider(targetIntent) - assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) - assertThat(testSubject.uriCount).isEqualTo(1) - assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri) - assertThat(testSubject.firstFileInfo?.previewUri).isNull() - verify(contentResolver, times(1)).getType(any()) - } + assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) + assertThat(testSubject.uriCount).isEqualTo(1) + assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri) + assertThat(testSubject.firstFileInfo?.previewUri).isNull() + assertThat(testSubject.getFirstFileName()).isEqualTo(fileName) + verify(contentResolver, times(1)).getType(any()) + } @Test - fun test_sendSingleImageWithFailingGetType_resolvesToFilePreviewUi() { - val uri = Uri.parse("content://org.pkg.app/image.png") - val targetIntent = - Intent(Intent.ACTION_SEND).apply { - type = "image/png" - putExtra(Intent.EXTRA_STREAM, uri) - } - whenever(contentResolver.getType(uri)).thenThrow(SecurityException("test failure")) - val testSubject = createDataProvider(targetIntent) + fun test_sendSingleImageWithFailingGetType_resolvesToFilePreviewUi() = + testScope.runTest { + val fileName = "image.png" + val uri = Uri.parse("content://org.pkg.app/$fileName") + val targetIntent = + Intent(Intent.ACTION_SEND).apply { + type = "image/png" + putExtra(Intent.EXTRA_STREAM, uri) + } + whenever(contentResolver.getType(uri)).thenThrow(SecurityException("test failure")) + val testSubject = createDataProvider(targetIntent) - assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) - assertThat(testSubject.uriCount).isEqualTo(1) - assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri) - assertThat(testSubject.firstFileInfo?.previewUri).isNull() - verify(contentResolver, times(1)).getType(any()) - } + assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) + assertThat(testSubject.uriCount).isEqualTo(1) + assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri) + assertThat(testSubject.firstFileInfo?.previewUri).isNull() + assertThat(testSubject.getFirstFileName()).isEqualTo(fileName) + verify(contentResolver, times(1)).getType(any()) + } @Test - fun test_sendSingleImageWithFailingMetadata_resolvesToFilePreviewUi() { - val uri = Uri.parse("content://org.pkg.app/image.png") - val targetIntent = - Intent(Intent.ACTION_SEND).apply { - type = "image/png" - putExtra(Intent.EXTRA_STREAM, uri) - } - whenever(contentResolver.getStreamTypes(uri, "*/*")) - .thenThrow(SecurityException("test failure")) - whenever(contentResolver.query(uri, METADATA_COLUMNS, null, null)) - .thenThrow(SecurityException("test failure")) - val testSubject = createDataProvider(targetIntent) + fun test_sendSingleFileWithFailingMetadata_resolvesToFilePreviewUi() = + testScope.runTest { + val fileName = "manual.pdf" + val uri = Uri.parse("content://org.pkg.app/$fileName") + val targetIntent = + Intent(Intent.ACTION_SEND).apply { + type = "application/pdf" + putExtra(Intent.EXTRA_STREAM, uri) + } + whenever(contentResolver.getType(uri)).thenReturn("application/pdf") + whenever(contentResolver.getStreamTypes(uri, "*/*")) + .thenThrow(SecurityException("test failure")) + whenever(contentResolver.query(eq(uri), anyOrNull(), anyOrNull(), anyOrNull())) + .thenThrow(SecurityException("test failure")) + val testSubject = createDataProvider(targetIntent) - assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) - assertThat(testSubject.uriCount).isEqualTo(1) - assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri) - assertThat(testSubject.firstFileInfo?.previewUri).isNull() - verify(contentResolver, times(1)).getType(any()) - } + assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) + assertThat(testSubject.uriCount).isEqualTo(1) + assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri) + assertThat(testSubject.firstFileInfo?.previewUri).isNull() + assertThat(testSubject.getFirstFileName()).isEqualTo(fileName) + verify(contentResolver, times(1)).getType(any()) + } @Test - fun test_SingleNonImageUriWithImageTypeInGetStreamTypes_useImagePreviewUi() { + @EnableFlags(FLAG_INDIVIDUAL_METADATA_TITLE_READ) + fun test_sendSingleImageWithFailingGetTypeDisjointTitleRead_resolvesToFilePreviewUi() = + testScope.runTest { + val uri = Uri.parse("content://org.pkg.app/image.png") + val targetIntent = + Intent(Intent.ACTION_SEND).apply { + type = "image/png" + putExtra(Intent.EXTRA_STREAM, uri) + } + whenever(contentResolver.getType(uri)).thenThrow(SecurityException("test failure")) + val title = "Image Title" + contentResolver.setTitle(uri, title) + val testSubject = createDataProvider(targetIntent) + + assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) + assertThat(testSubject.uriCount).isEqualTo(1) + assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri) + assertThat(testSubject.firstFileInfo?.previewUri).isNull() + assertThat(testSubject.getFirstFileName()).isEqualTo(title) + verify(contentResolver, times(1)).getType(any()) + } + + @Test + fun test_sendSingleFileWithFailingImageMetadata_resolvesToFilePreviewUi() = + testScope.runTest { + val fileName = "notes.pdf" + val uri = Uri.parse("content://org.pkg.app/$fileName") + val targetIntent = + Intent(Intent.ACTION_SEND).apply { + type = "application/pdf" + putExtra(Intent.EXTRA_STREAM, uri) + } + whenever(contentResolver.getType(uri)).thenReturn("application/pdf") + whenever(contentResolver.getStreamTypes(uri, "*/*")) + .thenThrow(SecurityException("test failure")) + whenever(contentResolver.query(eq(uri), anyOrNull(), anyOrNull(), anyOrNull())) + .thenThrow(SecurityException("test failure")) + val testSubject = createDataProvider(targetIntent) + + assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) + assertThat(testSubject.uriCount).isEqualTo(1) + assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri) + assertThat(testSubject.firstFileInfo?.previewUri).isNull() + assertThat(testSubject.getFirstFileName()).isEqualTo(fileName) + verify(contentResolver, times(1)).getType(any()) + } + + @Test + @EnableFlags(FLAG_INDIVIDUAL_METADATA_TITLE_READ) + fun test_sendSingleFileWithFailingImageMetadataIndividualTitleRead_resolvesToFilePreviewUi() = + testScope.runTest { + val uri = Uri.parse("content://org.pkg.app/image.png") + val targetIntent = + Intent(Intent.ACTION_SEND).apply { + type = "image/png" + putExtra(Intent.EXTRA_STREAM, uri) + } + whenever(contentResolver.getStreamTypes(uri, "*/*")) + .thenThrow(SecurityException("test failure")) + whenever(contentResolver.query(uri, ICON_METADATA_COLUMNS, null, null)) + .thenThrow(SecurityException("test failure")) + val displayName = "display name" + contentResolver.setDisplayName(uri, displayName) + val testSubject = createDataProvider(targetIntent) + + assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) + assertThat(testSubject.uriCount).isEqualTo(1) + assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri) + assertThat(testSubject.firstFileInfo?.previewUri).isNull() + assertThat(testSubject.getFirstFileName()).isEqualTo(displayName) + verify(contentResolver, times(1)).getType(any()) + } + + @Test + fun test_SingleFileUriWithImageTypeInGetStreamTypes_useImagePreviewUi() { val uri = Uri.parse("content://org.pkg.app/paper.pdf") val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) } whenever(contentResolver.getStreamTypes(uri, "*/*")) @@ -189,7 +305,7 @@ class PreviewDataProviderTest { arrayOf( DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL or DocumentsContract.Document.FLAG_SUPPORTS_METADATA - ) + ), ) } @@ -206,7 +322,8 @@ class PreviewDataProviderTest { val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) } whenever(contentResolver.getType(uri)).thenReturn("application/pdf") val cursor = MatrixCursor(columns).apply { addRow(values) } - whenever(contentResolver.query(uri, METADATA_COLUMNS, null, null)).thenReturn(cursor) + whenever(contentResolver.query(eq(uri), anyOrNull(), anyOrNull(), anyOrNull())) + .thenReturn(cursor) val testSubject = createDataProvider(targetIntent) @@ -224,12 +341,13 @@ class PreviewDataProviderTest { val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) } whenever(contentResolver.getType(uri)).thenReturn("application/pdf") val cursor = MatrixCursor(emptyArray()) - whenever(contentResolver.query(uri, METADATA_COLUMNS, null, null)).thenReturn(cursor) + whenever(contentResolver.query(eq(uri), anyOrNull(), anyOrNull(), anyOrNull())) + .thenReturn(cursor) val testSubject = createDataProvider(targetIntent) assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) - verify(contentResolver, times(1)).query(uri, METADATA_COLUMNS, null, null) + verify(contentResolver, times(1)).query(eq(uri), anyOrNull(), anyOrNull(), anyOrNull()) assertThat(cursor.isClosed).isTrue() } @@ -244,7 +362,7 @@ class PreviewDataProviderTest { ArrayList<Uri>().apply { add(uri1) add(uri2) - } + }, ) } whenever(contentResolver.getType(uri1)).thenReturn("image/png") @@ -272,7 +390,7 @@ class PreviewDataProviderTest { ArrayList<Uri>().apply { add(uri1) add(uri2) - } + }, ) } val testSubject = createDataProvider(targetIntent) @@ -286,7 +404,7 @@ class PreviewDataProviderTest { } @Test - fun test_someNonImageUriWithPreview_useImagePreviewUi() { + fun test_someFileUrisWithPreview_useImagePreviewUi() { val uri1 = Uri.parse("content://org.pkg.app/test.mp4") val uri2 = Uri.parse("content://org.pkg.app/test.pdf") val targetIntent = @@ -296,7 +414,7 @@ class PreviewDataProviderTest { ArrayList<Uri>().apply { add(uri1) add(uri2) - } + }, ) } whenever(contentResolver.getType(uri1)).thenReturn("video/mpeg4") @@ -312,29 +430,32 @@ class PreviewDataProviderTest { } @Test - fun test_allNonImageUrisWithoutPreview_useFilePreviewUi() { - val uri1 = Uri.parse("content://org.pkg.app/test.html") - val uri2 = Uri.parse("content://org.pkg.app/test.pdf") - val targetIntent = - Intent(Intent.ACTION_SEND_MULTIPLE).apply { - putExtra( - Intent.EXTRA_STREAM, - ArrayList<Uri>().apply { - add(uri1) - add(uri2) - } - ) - } - whenever(contentResolver.getType(uri1)).thenReturn("text/html") - whenever(contentResolver.getType(uri2)).thenReturn("application/pdf") - val testSubject = createDataProvider(targetIntent) + fun test_allFileUrisWithoutPreview_useFilePreviewUi() = + testScope.runTest { + val firstFileName = "test.html" + val uri1 = Uri.parse("content://org.pkg.app/$firstFileName") + val uri2 = Uri.parse("content://org.pkg.app/test.pdf") + val targetIntent = + Intent(Intent.ACTION_SEND_MULTIPLE).apply { + putExtra( + Intent.EXTRA_STREAM, + ArrayList<Uri>().apply { + add(uri1) + add(uri2) + }, + ) + } + whenever(contentResolver.getType(uri1)).thenReturn("text/html") + whenever(contentResolver.getType(uri2)).thenReturn("application/pdf") + val testSubject = createDataProvider(targetIntent) - assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) - assertThat(testSubject.uriCount).isEqualTo(2) - assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri1) - assertThat(testSubject.firstFileInfo?.previewUri).isNull() - verify(contentResolver, times(2)).getType(any()) - } + assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) + assertThat(testSubject.uriCount).isEqualTo(2) + assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri1) + assertThat(testSubject.firstFileInfo?.previewUri).isNull() + assertThat(testSubject.getFirstFileName()).isEqualTo(firstFileName) + verify(contentResolver, times(2)).getType(any()) + } @Test fun test_imagePreviewFileInfoFlow_dataLoadedOnce() = @@ -348,7 +469,7 @@ class PreviewDataProviderTest { ArrayList<Uri>().apply { add(uri1) add(uri2) - } + }, ) } whenever(contentResolver.getType(uri1)).thenReturn("text/html") @@ -372,11 +493,11 @@ class PreviewDataProviderTest { } @Test - fun sendItemsWithAdditionalContentUri_showPayloadTogglingUi() { + @EnableFlags(FLAG_CHOOSER_PAYLOAD_TOGGLING) + fun sendImageWithAdditionalContentUri_showPayloadTogglingUi() { val uri = Uri.parse("content://org.pkg.app/image.png") val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) } whenever(contentResolver.getType(uri)).thenReturn("image/png") - featureFlags.setFlag(Flags.FLAG_CHOOSER_PAYLOAD_TOGGLING, true) val testSubject = createDataProvider( targetIntent, @@ -392,7 +513,8 @@ class PreviewDataProviderTest { } @Test - fun sendItemsWithAdditionalContentUri_showImagePreviewUi() { + @DisableFlags(FLAG_CHOOSER_PAYLOAD_TOGGLING) + fun sendImageWithAdditionalContentUriAndDisabledFlag_showImagePreviewUi() { val uri = Uri.parse("content://org.pkg.app/image.png") val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) } whenever(contentResolver.getType(uri)).thenReturn("image/png") @@ -410,11 +532,11 @@ class PreviewDataProviderTest { } @Test + @EnableFlags(FLAG_CHOOSER_PAYLOAD_TOGGLING) fun sendItemsWithAdditionalContentUriWithSameAuthority_showImagePreviewUi() { val uri = Uri.parse("content://org.pkg.app/image.png") val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) } whenever(contentResolver.getType(uri)).thenReturn("image/png") - featureFlags.setFlag(Flags.FLAG_CHOOSER_PAYLOAD_TOGGLING, true) val testSubject = createDataProvider( targetIntent, @@ -434,10 +556,28 @@ class PreviewDataProviderTest { val testSubject = createDataProvider( targetIntent, - additionalContentUri = Uri.parse("content://org.pkg.app/extracontent") + additionalContentUri = Uri.parse("content://org.pkg.app/extracontent"), ) assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_TEXT) verify(contentResolver, never()).getType(any()) } + + companion object { + @JvmStatic + @Parameterized.Parameters(name = "{0}") + fun parameters(): List<FlagsParameterization> = + FlagsParameterization.allCombinationsOf(FLAG_INDIVIDUAL_METADATA_TITLE_READ) + } +} + +private fun ContentInterface.setDisplayName(uri: Uri, displayName: String) = + setMetadata(uri, arrayOf(OpenableColumns.DISPLAY_NAME), arrayOf(displayName)) + +private fun ContentInterface.setTitle(uri: Uri, title: String) = + setMetadata(uri, arrayOf(Downloads.Impl.COLUMN_TITLE), arrayOf(title)) + +private fun ContentInterface.setMetadata(uri: Uri, columns: Array<String>, values: Array<String>) { + whenever(query(uri, columns, null, null)) + .thenReturn(MatrixCursor(columns).apply { addRow(values) }) } |