summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--aconfig/FeatureFlags.aconfig10
-rw-r--r--java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt69
-rw-r--r--java/src/com/android/intentresolver/contentpreview/UriMetadataHelpers.kt37
-rw-r--r--java/src/com/android/intentresolver/ui/viewmodel/ChooserViewModel.kt1
-rw-r--r--tests/unit/src/com/android/intentresolver/contentpreview/PreviewDataProviderTest.kt358
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) })
}