diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt index ef47775f1b..5ec0dedadf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt @@ -33,9 +33,15 @@ object MimeTypes { const val Ogg = "audio/ogg" + const val PlainText = "text/plain" + fun String?.normalizeMimeType() = if (this == BadJpg) Jpeg else this fun String?.isMimeTypeImage() = this?.startsWith("image/").orFalse() fun String?.isMimeTypeVideo() = this?.startsWith("video/").orFalse() fun String?.isMimeTypeAudio() = this?.startsWith("audio/").orFalse() + fun String?.isMimeTypeApplication() = this?.startsWith("application/").orFalse() + fun String?.isMimeTypeFile() = this?.startsWith("file/").orFalse() + fun String?.isMimeTypeText() = this?.startsWith("text/").orFalse() + fun String?.isMimeTypeAny() = this?.startsWith("*/").orFalse() } diff --git a/vector/src/main/java/im/vector/app/features/attachments/MultiPickerIncomingFiles.kt b/vector/src/main/java/im/vector/app/features/attachments/MultiPickerIncomingFiles.kt new file mode 100644 index 0000000000..0f2dad541c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/attachments/MultiPickerIncomingFiles.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.attachments + +import android.content.Context +import android.content.Intent +import im.vector.lib.multipicker.MultiPicker +import javax.inject.Inject + +class MultiPickerIncomingFiles @Inject constructor( + private val context: Context, +) { + + fun image(intent: Intent) = MultiPicker.get(MultiPicker.IMAGE).getIncomingFiles(context, intent).map { it.toContentAttachmentData() } + + fun video(intent: Intent) = MultiPicker.get(MultiPicker.VIDEO).getIncomingFiles(context, intent).map { it.toContentAttachmentData() } + + fun media(intent: Intent) = MultiPicker.get(MultiPicker.MEDIA).getIncomingFiles(context, intent).map { it.toContentAttachmentData() } + + fun file(intent: Intent) = MultiPicker.get(MultiPicker.FILE).getIncomingFiles(context, intent).map { it.toContentAttachmentData() } + + fun audio(intent: Intent) = MultiPicker.get(MultiPicker.AUDIO).getIncomingFiles(context, intent).map { it.toContentAttachmentData() } +} diff --git a/vector/src/main/java/im/vector/app/features/attachments/ShareIntentHandler.kt b/vector/src/main/java/im/vector/app/features/attachments/ShareIntentHandler.kt index 06ca949025..18caba10d9 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/ShareIntentHandler.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/ShareIntentHandler.kt @@ -18,53 +18,36 @@ package im.vector.app.features.attachments import android.content.Context import android.content.Intent -import im.vector.lib.multipicker.MultiPicker import org.matrix.android.sdk.api.session.content.ContentAttachmentData +import org.matrix.android.sdk.api.util.MimeTypes +import org.matrix.android.sdk.api.util.MimeTypes.isMimeTypeAny +import org.matrix.android.sdk.api.util.MimeTypes.isMimeTypeApplication +import org.matrix.android.sdk.api.util.MimeTypes.isMimeTypeAudio +import org.matrix.android.sdk.api.util.MimeTypes.isMimeTypeFile +import org.matrix.android.sdk.api.util.MimeTypes.isMimeTypeImage +import org.matrix.android.sdk.api.util.MimeTypes.isMimeTypeText +import org.matrix.android.sdk.api.util.MimeTypes.isMimeTypeVideo import javax.inject.Inject -class ShareIntentHandler @Inject constructor() { +class ShareIntentHandler @Inject constructor( + private val multiPickerIncomingFiles: MultiPickerIncomingFiles, + private val context: Context, +) { /** * This methods aims to handle incoming share intents. * * @return true if it can handle the intent data, false otherwise */ - fun handleIncomingShareIntent(context: Context, intent: Intent, onFile: (List) -> Unit, onPlainText: (String) -> Unit): Boolean { + fun handleIncomingShareIntent(intent: Intent, onFile: (List) -> Unit, onPlainText: (String) -> Unit): Boolean { val type = intent.resolveType(context) ?: return false return when { - type == "text/plain" -> handlePlainText(intent, onPlainText) - type.startsWith("image") -> { - onFile( - MultiPicker.get(MultiPicker.IMAGE).getIncomingFiles(context, intent).map { - it.toContentAttachmentData() - } - ) - true - } - type.startsWith("video") -> { - onFile( - MultiPicker.get(MultiPicker.VIDEO).getIncomingFiles(context, intent).map { - it.toContentAttachmentData() - } - ) - true - } - type.startsWith("audio") -> { - onFile( - MultiPicker.get(MultiPicker.AUDIO).getIncomingFiles(context, intent).map { - it.toContentAttachmentData() - } - ) - true - } - - type.startsWith("application") || type.startsWith("file") || type.startsWith("text") || type.startsWith("*") -> { - onFile( - MultiPicker.get(MultiPicker.FILE).getIncomingFiles(context, intent).map { - it.toContentAttachmentData() - } - ) - true + type == MimeTypes.PlainText -> handlePlainText(intent, onPlainText) + type.isMimeTypeImage() -> onFile(multiPickerIncomingFiles.image(intent)).let { true } + type.isMimeTypeVideo() -> onFile(multiPickerIncomingFiles.video(intent)).let { true } + type.isMimeTypeAudio() -> onFile(multiPickerIncomingFiles.audio(intent)).let { true } + type.isMimeTypeApplication() || type.isMimeTypeFile() || type.isMimeTypeText() || type.isMimeTypeAny() -> { + onFile(multiPickerIncomingFiles.file(intent)).let { true } } else -> false } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 25947cc22b..972b5ea898 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -1622,7 +1622,7 @@ class TimelineFragment @Inject constructor( private fun sendUri(uri: Uri): Boolean { val shareIntent = Intent(Intent.ACTION_SEND, uri) - val isHandled = shareIntentHandler.handleIncomingShareIntent(requireContext(), shareIntent, ::onContentAttachmentsReady, onPlainText = { + val isHandled = shareIntentHandler.handleIncomingShareIntent(shareIntent, ::onContentAttachmentsReady, onPlainText = { fatalError("Should not happen as we're generating a File based share Intent", vectorPreferences.failFast()) }) if (!isHandled) { diff --git a/vector/src/main/java/im/vector/app/features/share/IncomingShareFragment.kt b/vector/src/main/java/im/vector/app/features/share/IncomingShareFragment.kt index a113c8105d..3f8923dd68 100644 --- a/vector/src/main/java/im/vector/app/features/share/IncomingShareFragment.kt +++ b/vector/src/main/java/im/vector/app/features/share/IncomingShareFragment.kt @@ -116,7 +116,6 @@ class IncomingShareFragment @Inject constructor( } private fun handleIncomingShareIntent(intent: Intent) = shareIntentHandler.handleIncomingShareIntent( - requireContext(), intent, onFile = { val sharedData = SharedData.Attachments(it) diff --git a/vector/src/test/java/im/vector/app/features/attachments/ShareIntentHandlerTest.kt b/vector/src/test/java/im/vector/app/features/attachments/ShareIntentHandlerTest.kt new file mode 100644 index 0000000000..d2fba95eee --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/attachments/ShareIntentHandlerTest.kt @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.attachments + +import android.content.Intent +import im.vector.app.test.fakes.FakeContext +import im.vector.app.test.fakes.FakeFunction1 +import im.vector.app.test.fakes.FakeIntent +import im.vector.app.test.fakes.FakeMultiPickerIncomingFiles +import im.vector.app.test.fixtures.ContentAttachmentDataFixture.aContentAttachmentData +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test +import org.matrix.android.sdk.api.session.content.ContentAttachmentData + +private val A_CONTEXT = FakeContext().instance +private const val A_PLAIN_TEXT_EXTRA = "plain text for sharing" +private val A_CONTENT_ATTACHMENT_LIST = listOf(aContentAttachmentData()) + +class ShareIntentHandlerTest { + + private val fakeMultiPickerIncomingFiles = FakeMultiPickerIncomingFiles() + private val onFile = FakeFunction1>() + private val onPlainText = FakeFunction1() + + private val shareIntentHandler = ShareIntentHandler(fakeMultiPickerIncomingFiles.instance, A_CONTEXT) + + @Test + fun `given an unhandled sharing intent type, when handling intent, then is not handled`() { + val unknownShareIntent = FakeIntent().also { it.givenResolvesType(A_CONTEXT, "unknown/type") } + + val handled = handleIncomingShareIntent(unknownShareIntent) + + onFile.verifyNoInteractions() + onPlainText.verifyNoInteractions() + handled shouldBeEqualTo false + } + + @Test + fun `given a plain text sharing intent, when handling intent, then is handled and parses plain text content`() { + val plainTextShareIntent = FakeIntent().also { + it.givenResolvesType(A_CONTEXT, "text/plain") + it.givenCharSequenceExtra(key = Intent.EXTRA_TEXT, value = A_PLAIN_TEXT_EXTRA) + } + + val handled = handleIncomingShareIntent(plainTextShareIntent) + + onFile.verifyNoInteractions() + onPlainText.assertValue(A_PLAIN_TEXT_EXTRA) + handled shouldBeEqualTo true + } + + @Test + fun `given an empty plain text sharing intent, when handling intent, then is not handled`() { + val plainTextShareIntent = FakeIntent().also { + it.givenResolvesType(A_CONTEXT, "text/plain") + it.givenCharSequenceExtra(key = Intent.EXTRA_TEXT, value = "") + } + + val handled = handleIncomingShareIntent(plainTextShareIntent) + + onFile.verifyNoInteractions() + onPlainText.verifyNoInteractions() + handled shouldBeEqualTo false + } + + @Test + fun `given an image sharing intent, when handling intent, then is handled and parses image files`() { + val imageShareIntent = FakeIntent().also { it.givenResolvesType(A_CONTEXT, "image/png") } + fakeMultiPickerIncomingFiles.givenImageReturns(imageShareIntent.instance, A_CONTENT_ATTACHMENT_LIST) + + val handled = handleIncomingShareIntent(imageShareIntent) + + onFile.assertValue(A_CONTENT_ATTACHMENT_LIST) + onPlainText.verifyNoInteractions() + handled shouldBeEqualTo true + } + + @Test + fun `given an audio sharing intent, when handling intent, then is handled and parses audio files`() { + val audioShareIntent = FakeIntent().also { it.givenResolvesType(A_CONTEXT, "audio/mp3") } + fakeMultiPickerIncomingFiles.givenAudioReturns(audioShareIntent.instance, A_CONTENT_ATTACHMENT_LIST) + + val handled = handleIncomingShareIntent(audioShareIntent) + + onFile.assertValue(A_CONTENT_ATTACHMENT_LIST) + onPlainText.verifyNoInteractions() + handled shouldBeEqualTo true + } + + @Test + fun `given an video sharing intent, when handling intent, then is handled and parses video files`() { + val videoShareIntent = FakeIntent().also { it.givenResolvesType(A_CONTEXT, "video/mp4") } + fakeMultiPickerIncomingFiles.givenVideoReturns(videoShareIntent.instance, A_CONTENT_ATTACHMENT_LIST) + + val handled = handleIncomingShareIntent(videoShareIntent) + + onFile.assertValue(A_CONTENT_ATTACHMENT_LIST) + onPlainText.verifyNoInteractions() + handled shouldBeEqualTo true + } + + @Test + fun `given a file sharing intent, when handling intent, then is handled and parses files`() { + val fileShareIntent = FakeIntent().also { it.givenResolvesType(A_CONTEXT, "file/*") } + fakeMultiPickerIncomingFiles.givenFileReturns(fileShareIntent.instance, A_CONTENT_ATTACHMENT_LIST) + + val handled = handleIncomingShareIntent(fileShareIntent) + + onFile.assertValue(A_CONTENT_ATTACHMENT_LIST) + onPlainText.verifyNoInteractions() + handled shouldBeEqualTo true + } + + @Test + fun `given a application sharing intent, when handling intent, then is handled and parses files`() { + val fileShareIntent = FakeIntent().also { it.givenResolvesType(A_CONTEXT, "application/apk") } + fakeMultiPickerIncomingFiles.givenFileReturns(fileShareIntent.instance, A_CONTENT_ATTACHMENT_LIST) + + handleIncomingShareIntent(fileShareIntent) + + onFile.assertValue(A_CONTENT_ATTACHMENT_LIST) + onPlainText.verifyNoInteractions() + } + + @Test + fun `given a text sharing intent, when handling intent, then is handled and parses text files`() { + val fileShareIntent = FakeIntent().also { it.givenResolvesType(A_CONTEXT, "text/ics") } + fakeMultiPickerIncomingFiles.givenFileReturns(fileShareIntent.instance, A_CONTENT_ATTACHMENT_LIST) + + val handled = handleIncomingShareIntent(fileShareIntent) + + onFile.assertValue(A_CONTENT_ATTACHMENT_LIST) + onPlainText.verifyNoInteractions() + handled shouldBeEqualTo true + } + + @Test + fun `given a wildcard sharing intent, when handling intent, then is handled and parses files`() { + val fileShareIntent = FakeIntent().also { it.givenResolvesType(A_CONTEXT, "*/*") } + fakeMultiPickerIncomingFiles.givenFileReturns(fileShareIntent.instance, A_CONTENT_ATTACHMENT_LIST) + + val handled = handleIncomingShareIntent(fileShareIntent) + + onFile.assertValue(A_CONTENT_ATTACHMENT_LIST) + onPlainText.verifyNoInteractions() + handled shouldBeEqualTo true + } + + private fun handleIncomingShareIntent(intent: FakeIntent): Boolean { + return shareIntentHandler.handleIncomingShareIntent(intent.instance, onFile.capture, onPlainText.capture) + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeFunction1.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeFunction1.kt new file mode 100644 index 0000000000..434a661e5f --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeFunction1.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.test.fakes + +import org.amshove.kluent.shouldBeEqualTo + +class FakeFunction1 { + + private lateinit var capturedValue: T + + val capture: (T) -> Unit = { + capturedValue = it + } + + fun verifyNoInteractions() { + this::capturedValue.isInitialized shouldBeEqualTo false + } + + fun assertValue(value: T) { + capturedValue shouldBeEqualTo value + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeIntent.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeIntent.kt new file mode 100644 index 0000000000..1b3ce3538e --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeIntent.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.test.fakes + +import android.content.Context +import android.content.Intent +import io.mockk.every +import io.mockk.mockk + +class FakeIntent { + + val instance = mockk() + + fun givenResolvesType(context: Context, type: String?) { + every { instance.resolveType(context) } returns type + } + + fun givenCharSequenceExtra(key: String, value: CharSequence) { + every { instance.getCharSequenceExtra(key) } returns value + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeMultiPickerIncomingFiles.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeMultiPickerIncomingFiles.kt new file mode 100644 index 0000000000..00adc83e5c --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeMultiPickerIncomingFiles.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.test.fakes + +import android.content.Intent +import im.vector.app.features.attachments.MultiPickerIncomingFiles +import io.mockk.every +import io.mockk.mockk +import org.matrix.android.sdk.api.session.content.ContentAttachmentData + +class FakeMultiPickerIncomingFiles { + + val instance = mockk() + + fun givenFileReturns(intent: Intent, result: List) { + every { instance.file(intent) } returns result + } + + fun givenAudioReturns(intent: Intent, result: List) { + every { instance.audio(intent) } returns result + } + + fun givenVideoReturns(intent: Intent, result: List) { + every { instance.video(intent) } returns result + } + + fun givenImageReturns(intent: Intent, result: List) { + every { instance.image(intent) } returns result + } +} diff --git a/vector/src/test/java/im/vector/app/test/fixtures/ContentAttachmentDataFixture.kt b/vector/src/test/java/im/vector/app/test/fixtures/ContentAttachmentDataFixture.kt new file mode 100644 index 0000000000..731d6b32d0 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fixtures/ContentAttachmentDataFixture.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.test.fixtures + +import im.vector.app.test.fakes.FakeUri +import org.matrix.android.sdk.api.session.content.ContentAttachmentData + +object ContentAttachmentDataFixture { + + fun aContentAttachmentData() = ContentAttachmentData( + type = ContentAttachmentData.Type.AUDIO, + queryUri = FakeUri().instance, + mimeType = null, + ) +}