mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-04 01:19:33 +00:00
Create the LocalMedia off the main thread when picking or sharing media
LocalMediaFactory.createFromUri queries the ContentResolver and can decode the media, it was called on the main thread from the picker callbacks and when sharing a file. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+51
-38
@@ -43,6 +43,7 @@ import io.element.android.features.messages.impl.timeline.TimelineController
|
|||||||
import io.element.android.features.messages.impl.utils.TextPillificationHelper
|
import io.element.android.features.messages.impl.utils.TextPillificationHelper
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
@@ -110,6 +111,7 @@ import kotlinx.coroutines.flow.map
|
|||||||
import kotlinx.coroutines.flow.merge
|
import kotlinx.coroutines.flow.merge
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
import io.element.android.libraries.core.mimetype.MimeTypes.Any as AnyMimeTypes
|
import io.element.android.libraries.core.mimetype.MimeTypes.Any as AnyMimeTypes
|
||||||
@@ -145,6 +147,7 @@ class MessageComposerPresenter(
|
|||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
private val contentScannerService: ContentScannerService,
|
private val contentScannerService: ContentScannerService,
|
||||||
private val contentValidationCache: EventContentValidationCache,
|
private val contentValidationCache: EventContentValidationCache,
|
||||||
|
private val dispatchers: CoroutineDispatchers,
|
||||||
) : Presenter<MessageComposerState> {
|
) : Presenter<MessageComposerState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
@@ -193,22 +196,22 @@ class MessageComposerPresenter(
|
|||||||
.collectAsState(initial = false)
|
.collectAsState(initial = false)
|
||||||
|
|
||||||
val galleryMediaPicker = mediaPickerProvider.registerGalleryPicker { uri, mimeType ->
|
val galleryMediaPicker = mediaPickerProvider.registerGalleryPicker { uri, mimeType ->
|
||||||
handlePickedMedia(uri, mimeType)
|
localCoroutineScope.handlePickedMedia(uri, mimeType)
|
||||||
}
|
}
|
||||||
val galleryMultiMediaPicker = mediaPickerProvider.registerGalleryMultiPicker { uris ->
|
val galleryMultiMediaPicker = mediaPickerProvider.registerGalleryMultiPicker { uris ->
|
||||||
handlePickedMediaList(uris)
|
localCoroutineScope.handlePickedMediaList(uris)
|
||||||
}
|
}
|
||||||
val filesPicker = mediaPickerProvider.registerFileMultiPicker(AnyMimeTypes) { uris ->
|
val filesPicker = mediaPickerProvider.registerFileMultiPicker(AnyMimeTypes) { uris ->
|
||||||
handlePickedMediaList(uris, sendAsFile = true)
|
localCoroutineScope.handlePickedMediaList(uris, sendAsFile = true)
|
||||||
}
|
}
|
||||||
val fileSinglePicker = mediaPickerProvider.registerFilePicker(AnyMimeTypes) { uri, mimeType ->
|
val fileSinglePicker = mediaPickerProvider.registerFilePicker(AnyMimeTypes) { uri, mimeType ->
|
||||||
handlePickedMedia(uri, mimeType ?: MimeTypes.OctetStream, sendAsFile = true)
|
localCoroutineScope.handlePickedMedia(uri, mimeType ?: MimeTypes.OctetStream, sendAsFile = true)
|
||||||
}
|
}
|
||||||
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker { uri ->
|
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker { uri ->
|
||||||
handlePickedMedia(uri, MimeTypes.Jpeg)
|
localCoroutineScope.handlePickedMedia(uri, MimeTypes.Jpeg)
|
||||||
}
|
}
|
||||||
val cameraVideoPicker = mediaPickerProvider.registerCameraVideoPicker { uri ->
|
val cameraVideoPicker = mediaPickerProvider.registerCameraVideoPicker { uri ->
|
||||||
handlePickedMedia(uri, MimeTypes.Mp4)
|
localCoroutineScope.handlePickedMedia(uri, MimeTypes.Mp4)
|
||||||
}
|
}
|
||||||
val isFullScreen = rememberSaveable {
|
val isFullScreen = rememberSaveable {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
@@ -286,17 +289,16 @@ class MessageComposerPresenter(
|
|||||||
}
|
}
|
||||||
is MessageComposerEvent.SendUri -> {
|
is MessageComposerEvent.SendUri -> {
|
||||||
val inReplyToEventId = (messageComposerContext.composerMode as? MessageComposerMode.Reply)?.eventId
|
val inReplyToEventId = (messageComposerContext.composerMode as? MessageComposerMode.Reply)?.eventId
|
||||||
sessionCoroutineScope.sendAttachment(
|
sessionCoroutineScope.launch {
|
||||||
attachment = Attachment.Media(
|
val localMedia = createLocalMedia(
|
||||||
localMedia = localMediaFactory.createFromUri(
|
uri = event.uri,
|
||||||
uri = event.uri,
|
mimeType = null,
|
||||||
mimeType = null,
|
)
|
||||||
name = null,
|
sendAttachment(
|
||||||
formattedFileSize = null
|
attachment = Attachment.Media(localMedia),
|
||||||
),
|
inReplyToEventId = inReplyToEventId,
|
||||||
),
|
)
|
||||||
inReplyToEventId = inReplyToEventId,
|
}
|
||||||
)
|
|
||||||
|
|
||||||
resetComposerModeAfterAttaching()
|
resetComposerModeAfterAttaching()
|
||||||
}
|
}
|
||||||
@@ -640,17 +642,28 @@ class MessageComposerPresenter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handlePickedMedia(
|
// Creating the LocalMedia queries the ContentResolver and can decode the media, so do it off the main thread
|
||||||
uri: Uri?,
|
private suspend fun createLocalMedia(
|
||||||
mimeType: String? = null,
|
uri: Uri,
|
||||||
sendAsFile: Boolean = false,
|
mimeType: String?,
|
||||||
) {
|
) = withContext(dispatchers.io) {
|
||||||
uri ?: return
|
localMediaFactory.createFromUri(
|
||||||
val localMedia = localMediaFactory.createFromUri(
|
|
||||||
uri = uri,
|
uri = uri,
|
||||||
mimeType = mimeType,
|
mimeType = mimeType,
|
||||||
name = null,
|
name = null,
|
||||||
formattedFileSize = null
|
formattedFileSize = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.handlePickedMedia(
|
||||||
|
uri: Uri?,
|
||||||
|
mimeType: String? = null,
|
||||||
|
sendAsFile: Boolean = false,
|
||||||
|
) = launch {
|
||||||
|
uri ?: return@launch
|
||||||
|
val localMedia = createLocalMedia(
|
||||||
|
uri = uri,
|
||||||
|
mimeType = mimeType,
|
||||||
)
|
)
|
||||||
val mediaAttachment = Attachment.Media(localMedia, sendAsFile = sendAsFile)
|
val mediaAttachment = Attachment.Media(localMedia, sendAsFile = sendAsFile)
|
||||||
val inReplyToEventId = (messageComposerContext.composerMode as? MessageComposerMode.Reply)?.eventId
|
val inReplyToEventId = (messageComposerContext.composerMode as? MessageComposerMode.Reply)?.eventId
|
||||||
@@ -659,7 +672,7 @@ class MessageComposerPresenter(
|
|||||||
resetComposerModeAfterAttaching()
|
resetComposerModeAfterAttaching()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handlePickedMediaList(
|
private fun CoroutineScope.handlePickedMediaList(
|
||||||
uris: List<Uri>,
|
uris: List<Uri>,
|
||||||
sendAsFile: Boolean = false,
|
sendAsFile: Boolean = false,
|
||||||
) {
|
) {
|
||||||
@@ -668,19 +681,19 @@ class MessageComposerPresenter(
|
|||||||
handlePickedMedia(uris.first(), sendAsFile = sendAsFile)
|
handlePickedMedia(uris.first(), sendAsFile = sendAsFile)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val attachments = uris.map { uri ->
|
launch {
|
||||||
val localMedia = localMediaFactory.createFromUri(
|
val attachments = uris.map { uri ->
|
||||||
uri = uri,
|
val localMedia = createLocalMedia(
|
||||||
mimeType = null,
|
uri = uri,
|
||||||
name = null,
|
mimeType = null,
|
||||||
formattedFileSize = null,
|
)
|
||||||
)
|
Attachment.Media(localMedia, sendAsFile = sendAsFile)
|
||||||
Attachment.Media(localMedia, sendAsFile = sendAsFile)
|
}.toImmutableList()
|
||||||
}.toImmutableList()
|
val inReplyToEventId = (messageComposerContext.composerMode as? MessageComposerMode.Reply)?.eventId
|
||||||
val inReplyToEventId = (messageComposerContext.composerMode as? MessageComposerMode.Reply)?.eventId
|
navigator.navigateToPreviewAttachments(attachments, inReplyToEventId)
|
||||||
navigator.navigateToPreviewAttachments(attachments, inReplyToEventId)
|
|
||||||
|
|
||||||
resetComposerModeAfterAttaching()
|
resetComposerModeAfterAttaching()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resetComposerModeAfterAttaching() {
|
private fun resetComposerModeAfterAttaching() {
|
||||||
|
|||||||
+1
@@ -324,6 +324,7 @@ class MessageComposerPresenterSlashCommandTest {
|
|||||||
featureFlagService = featureFlagService,
|
featureFlagService = featureFlagService,
|
||||||
contentScannerService = { _, _ -> },
|
contentScannerService = { _, _ -> },
|
||||||
contentValidationCache = InMemoryEventContentValidationCache(),
|
contentValidationCache = InMemoryEventContentValidationCache(),
|
||||||
|
dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
|
||||||
).apply {
|
).apply {
|
||||||
isTesting = true
|
isTesting = true
|
||||||
showTextFormatting = isRichTextEditorEnabled
|
showTextFormatting = isRichTextEditorEnabled
|
||||||
|
|||||||
+1
@@ -1723,6 +1723,7 @@ class MessageComposerPresenterTest : RobolectricTest() {
|
|||||||
featureFlagService = featureFlagService,
|
featureFlagService = featureFlagService,
|
||||||
contentScannerService = { _, _ -> },
|
contentScannerService = { _, _ -> },
|
||||||
contentValidationCache = InMemoryEventContentValidationCache(),
|
contentValidationCache = InMemoryEventContentValidationCache(),
|
||||||
|
dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
|
||||||
).apply {
|
).apply {
|
||||||
isTesting = true
|
isTesting = true
|
||||||
showTextFormatting = isRichTextEditorEnabled
|
showTextFormatting = isRichTextEditorEnabled
|
||||||
|
|||||||
Reference in New Issue
Block a user