mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 12:07:28 +00:00
Merge pull request #7435 from hayaksi1/fix/5623-share-unlisted-mime-type
Share a file whose mime type is not one of the supported ones
This commit is contained in:
+2
-1
@@ -42,7 +42,8 @@ class DefaultShareIntentHandler(
|
||||
val uris = getIncomingUris(intent, type)
|
||||
return when {
|
||||
uris.isEmpty() && type == MimeTypes.PlainText -> handlePlainText(intent)
|
||||
type.isMimeTypeImage() ||
|
||||
uris.isNotEmpty() ||
|
||||
type.isMimeTypeImage() ||
|
||||
type.isMimeTypeVideo() ||
|
||||
type.isMimeTypeAudio() ||
|
||||
type.isMimeTypeApplication() ||
|
||||
|
||||
+303
@@ -0,0 +1,303 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.share.impl
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.ContentProvider
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.google.common.truth.Truth.assertWithMessage
|
||||
import io.element.android.features.share.api.ShareIntentData
|
||||
import io.element.android.features.share.api.UriToShare
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
import io.element.android.tests.testutils.robolectric.RobolectricTest
|
||||
import org.junit.Test
|
||||
import org.robolectric.Robolectric
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
import org.robolectric.Shadows.shadowOf
|
||||
|
||||
class DefaultShareIntentHandlerTest : RobolectricTest() {
|
||||
@Test
|
||||
fun `an image is shared as an uri`() {
|
||||
val uri = "content://sender/image.jpg".toUri()
|
||||
val result = createDefaultShareIntentHandler().handleIncomingShareIntent(
|
||||
aSendIntent(type = "image/jpeg", uri = uri)
|
||||
)
|
||||
assertThat(result).isEqualTo(
|
||||
ShareIntentData.Uris(
|
||||
text = null,
|
||||
uris = listOf(UriToShare(uri = uri, mimeType = "image/jpeg")),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an uri with an unlisted mime type is shared`() {
|
||||
val uri = "content://sender/logs.zip".toUri()
|
||||
val result = createDefaultShareIntentHandler().handleIncomingShareIntent(
|
||||
aSendIntent(type = "message/rfc822", uri = uri)
|
||||
)
|
||||
assertThat(result).isEqualTo(
|
||||
ShareIntentData.Uris(
|
||||
text = null,
|
||||
uris = listOf(UriToShare(uri = uri, mimeType = "message/rfc822")),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an uri with an unlisted mime type keeps the text as a caption`() {
|
||||
val uri = "content://sender/logs.zip".toUri()
|
||||
val result = createDefaultShareIntentHandler().handleIncomingShareIntent(
|
||||
aSendIntent(type = "message/rfc822", uri = uri).apply {
|
||||
putExtra(Intent.EXTRA_TEXT, "a caption")
|
||||
}
|
||||
)
|
||||
assertThat(result).isEqualTo(
|
||||
ShareIntentData.Uris(
|
||||
text = "a caption",
|
||||
uris = listOf(UriToShare(uri = uri, mimeType = "message/rfc822")),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `several uris with an unlisted mime type are shared`() {
|
||||
val firstUri = "content://sender/first.zip".toUri()
|
||||
val secondUri = "content://sender/second.zip".toUri()
|
||||
val intent = Intent(Intent.ACTION_SEND_MULTIPLE).apply {
|
||||
type = "message/rfc822"
|
||||
putParcelableArrayListExtra(Intent.EXTRA_STREAM, arrayListOf(firstUri, secondUri))
|
||||
}
|
||||
val result = createDefaultShareIntentHandler().handleIncomingShareIntent(intent)
|
||||
assertThat(result).isEqualTo(
|
||||
ShareIntentData.Uris(
|
||||
text = null,
|
||||
uris = listOf(
|
||||
UriToShare(uri = firstUri, mimeType = "message/rfc822"),
|
||||
UriToShare(uri = secondUri, mimeType = "message/rfc822"),
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a plain text is shared as a text`() {
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = "text/plain"
|
||||
putExtra(Intent.EXTRA_TEXT, "a text")
|
||||
}
|
||||
val result = createDefaultShareIntentHandler().handleIncomingShareIntent(intent)
|
||||
assertThat(result).isEqualTo(ShareIntentData.PlainText("a text"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an intent with neither uri nor text is not handled`() {
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = "message/rfc822"
|
||||
}
|
||||
val result = createDefaultShareIntentHandler().handleIncomingShareIntent(intent)
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an intent without mime type is not handled`() {
|
||||
val result = createDefaultShareIntentHandler().handleIncomingShareIntent(Intent(Intent.ACTION_SEND))
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an intent with a listed mime type and no uri is handled`() {
|
||||
listOf(
|
||||
MimeTypes.Jpeg,
|
||||
MimeTypes.Mp4,
|
||||
MimeTypes.Mp3,
|
||||
MimeTypes.Pdf,
|
||||
"file/binary",
|
||||
"text/html",
|
||||
MimeTypes.Any,
|
||||
).forEach { type ->
|
||||
val result = createDefaultShareIntentHandler().handleIncomingShareIntent(
|
||||
Intent(Intent.ACTION_SEND).apply { this.type = type }
|
||||
)
|
||||
assertWithMessage("$type was not properly handled").that(result).isEqualTo(
|
||||
ShareIntentData.Uris(
|
||||
text = null,
|
||||
uris = emptyList(),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a send multiple intent without uri is handled`() {
|
||||
val intent = Intent(Intent.ACTION_SEND_MULTIPLE).apply {
|
||||
type = MimeTypes.Jpeg
|
||||
}
|
||||
val result = createDefaultShareIntentHandler().handleIncomingShareIntent(intent)
|
||||
assertThat(result).isEqualTo(
|
||||
ShareIntentData.Uris(
|
||||
text = null,
|
||||
uris = emptyList(),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an uri is ignored when the intent is neither a send nor a send multiple`() {
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
type = MimeTypes.Jpeg
|
||||
putExtra(Intent.EXTRA_STREAM, "content://sender/image.jpg".toUri())
|
||||
}
|
||||
val result = createDefaultShareIntentHandler().handleIncomingShareIntent(intent)
|
||||
assertThat(result).isEqualTo(
|
||||
ShareIntentData.Uris(
|
||||
text = null,
|
||||
uris = emptyList(),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an empty text is not kept as a caption`() {
|
||||
val uri = "content://sender/logs.zip".toUri()
|
||||
val result = createDefaultShareIntentHandler().handleIncomingShareIntent(
|
||||
aSendIntent(type = "message/rfc822", uri = uri).apply {
|
||||
putExtra(Intent.EXTRA_TEXT, "")
|
||||
}
|
||||
)
|
||||
assertThat(result).isEqualTo(
|
||||
ShareIntentData.Uris(
|
||||
text = null,
|
||||
uris = listOf(UriToShare(uri = uri, mimeType = "message/rfc822")),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a plain text intent without text is not handled`() {
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = MimeTypes.PlainText
|
||||
}
|
||||
val result = createDefaultShareIntentHandler().handleIncomingShareIntent(intent)
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a plain text intent with an empty text is not handled`() {
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = MimeTypes.PlainText
|
||||
putExtra(Intent.EXTRA_TEXT, "")
|
||||
}
|
||||
val result = createDefaultShareIntentHandler().handleIncomingShareIntent(intent)
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the mime type of the uri overrides the one of the intent`() {
|
||||
Robolectric.setupContentProvider(ZipContentProvider::class.java, "sender")
|
||||
val uri = "content://sender/logs.zip".toUri()
|
||||
val result = createDefaultShareIntentHandler().handleIncomingShareIntent(
|
||||
aSendIntent(type = MimeTypes.Any, uri = uri)
|
||||
)
|
||||
assertThat(result).isEqualTo(
|
||||
ShareIntentData.Uris(
|
||||
text = null,
|
||||
uris = listOf(UriToShare(uri = uri, mimeType = "application/zip")),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the intent is made explicit for the application which will read the uri`() {
|
||||
val intent = aSendIntent(type = "message/rfc822", uri = "content://sender/logs.zip".toUri())
|
||||
givenAnApplicationHandling(intent)
|
||||
createDefaultShareIntentHandler().handleIncomingShareIntent(intent)
|
||||
assertThat(intent.action).isNull()
|
||||
assertThat(intent.component).isEqualTo(ComponentName(A_PACKAGE_NAME, AN_ACTIVITY_NAME))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the intent stays implicit when the uri permission cannot be granted`() {
|
||||
val uri = "content://sender/logs.zip".toUri()
|
||||
val intent = aSendIntent(type = "message/rfc822", uri = uri)
|
||||
givenAnApplicationHandling(intent)
|
||||
val result = DefaultShareIntentHandler(
|
||||
context = FailingGrantUriPermissionContext(RuntimeEnvironment.getApplication()),
|
||||
).handleIncomingShareIntent(intent)
|
||||
assertThat(intent.action).isEqualTo(Intent.ACTION_SEND)
|
||||
assertThat(intent.component).isNull()
|
||||
assertThat(result).isEqualTo(
|
||||
ShareIntentData.Uris(
|
||||
text = null,
|
||||
uris = listOf(UriToShare(uri = uri, mimeType = "message/rfc822")),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun aSendIntent(type: String, uri: Uri) = Intent(Intent.ACTION_SEND).apply {
|
||||
this.type = type
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
}
|
||||
|
||||
private fun givenAnApplicationHandling(intent: Intent) {
|
||||
val componentName = ComponentName(A_PACKAGE_NAME, AN_ACTIVITY_NAME)
|
||||
shadowOf(RuntimeEnvironment.getApplication().packageManager).apply {
|
||||
addActivityIfNotPresent(componentName)
|
||||
addIntentFilterForActivity(
|
||||
componentName,
|
||||
IntentFilter(intent.action).apply {
|
||||
addCategory(Intent.CATEGORY_DEFAULT)
|
||||
addDataType(intent.type)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDefaultShareIntentHandler() = DefaultShareIntentHandler(
|
||||
context = RuntimeEnvironment.getApplication(),
|
||||
)
|
||||
|
||||
private class FailingGrantUriPermissionContext(context: Context) : ContextWrapper(context) {
|
||||
override fun grantUriPermission(toPackage: String, uri: Uri, modeFlags: Int) {
|
||||
error("Unable to grant Uri permission")
|
||||
}
|
||||
}
|
||||
|
||||
private class ZipContentProvider : ContentProvider() {
|
||||
override fun onCreate() = true
|
||||
|
||||
override fun getType(uri: Uri) = "application/zip"
|
||||
|
||||
override fun query(
|
||||
uri: Uri,
|
||||
projection: Array<out String>?,
|
||||
selection: String?,
|
||||
selectionArgs: Array<out String>?,
|
||||
sortOrder: String?,
|
||||
): Cursor? = null
|
||||
|
||||
override fun insert(uri: Uri, values: ContentValues?): Uri? = null
|
||||
|
||||
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?) = 0
|
||||
|
||||
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?) = 0
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val A_PACKAGE_NAME = "io.element.android.x"
|
||||
const val AN_ACTIVITY_NAME = "io.element.android.x.MainActivity"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user