mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-03 16:10:00 +00:00
Close the threaded timelines created by ShareLocationPresenter and DefaultMediaSender
Both created a dedicated timeline for each operation performed in a thread, and never closed it, leaking SDK resources. Apply the same treatment as PollRepository: replace getTimeline() with a scoped withTimeline() helper, which closes the created timeline once the operation completes, whether it succeeds, fails or gets cancelled. In DefaultMediaSender.sendGallery(), waiting for the upload handler is now done inside the block, so that the timeline stays open until the upload completes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+36
-21
@@ -100,9 +100,9 @@ class DefaultMediaSender(
|
||||
inReplyToEventId: EventId?,
|
||||
): Result<Unit> {
|
||||
val mediaLogId = mediaId(mediaUploadInfo.file)
|
||||
return getTimeline().flatMap {
|
||||
Timber.d("Started sending media $mediaLogId using timeline: ${it.mode}")
|
||||
it.sendMedia(
|
||||
return withTimeline { timeline ->
|
||||
Timber.d("Started sending media $mediaLogId using timeline: ${timeline.mode}")
|
||||
timeline.sendMedia(
|
||||
uploadInfo = mediaUploadInfo,
|
||||
caption = caption,
|
||||
formattedCaption = formattedCaption,
|
||||
@@ -128,12 +128,14 @@ class DefaultMediaSender(
|
||||
mediaOptimizationConfig = mediaOptimizationConfig,
|
||||
)
|
||||
.flatMapCatching { info ->
|
||||
getTimeline().getOrThrow().sendMedia(
|
||||
uploadInfo = info,
|
||||
caption = caption,
|
||||
formattedCaption = formattedCaption,
|
||||
inReplyToEventId = inReplyToEventId,
|
||||
)
|
||||
withTimeline { timeline ->
|
||||
timeline.sendMedia(
|
||||
uploadInfo = info,
|
||||
caption = caption,
|
||||
formattedCaption = formattedCaption,
|
||||
inReplyToEventId = inReplyToEventId,
|
||||
)
|
||||
}
|
||||
}
|
||||
.handleSendResult(mediaId(uri))
|
||||
}
|
||||
@@ -158,12 +160,14 @@ class DefaultMediaSender(
|
||||
audioInfo = audioInfo,
|
||||
waveform = waveForm,
|
||||
)
|
||||
getTimeline().getOrThrow().sendMedia(
|
||||
uploadInfo = newInfo,
|
||||
caption = null,
|
||||
formattedCaption = null,
|
||||
inReplyToEventId = inReplyToEventId,
|
||||
)
|
||||
withTimeline { timeline ->
|
||||
timeline.sendMedia(
|
||||
uploadInfo = newInfo,
|
||||
caption = null,
|
||||
formattedCaption = null,
|
||||
inReplyToEventId = inReplyToEventId,
|
||||
)
|
||||
}
|
||||
}
|
||||
.handleSendResult(mediaId(uri))
|
||||
}
|
||||
@@ -176,7 +180,7 @@ class DefaultMediaSender(
|
||||
): Result<Unit> {
|
||||
val galleryLogId = "gallery[${mediaUploadInfos.size} items]"
|
||||
Timber.d("Sending $galleryLogId")
|
||||
return getTimeline().flatMap { timeline ->
|
||||
return withTimeline { timeline ->
|
||||
val galleryItems = mediaUploadInfos.map { it.toGalleryItemInfo() }
|
||||
timeline.sendGallery(
|
||||
items = galleryItems,
|
||||
@@ -184,10 +188,10 @@ class DefaultMediaSender(
|
||||
formattedCaption = formattedCaption,
|
||||
inReplyToEventId = inReplyToEventId,
|
||||
)
|
||||
.flatMapCatching { uploadHandler ->
|
||||
uploadHandler.await()
|
||||
}
|
||||
}
|
||||
.flatMapCatching { uploadHandler ->
|
||||
uploadHandler.await()
|
||||
}
|
||||
.handleSendResult(galleryLogId)
|
||||
}
|
||||
|
||||
@@ -269,12 +273,23 @@ class DefaultMediaSender(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getTimeline(): Result<Timeline> {
|
||||
/**
|
||||
* Invokes [block] with the [Timeline] matching [timelineMode].
|
||||
*
|
||||
* For a thread, a dedicated timeline is created and closed once [block] returns, since such a
|
||||
* timeline owns SDK resources. [block] must then perform the whole sending, including waiting
|
||||
* for the media upload to complete. For the other modes, the live timeline is used, and is not
|
||||
* closed here since it is owned by its room.
|
||||
*/
|
||||
private suspend fun <T> withTimeline(block: suspend (Timeline) -> Result<T>): Result<T> {
|
||||
return when (timelineMode) {
|
||||
is Timeline.Mode.Thread -> {
|
||||
room.createTimeline(CreateTimelineParams.Threaded(threadRootEventId = timelineMode.threadRootId))
|
||||
.flatMap { threadedTimeline ->
|
||||
threadedTimeline.use { block(it) }
|
||||
}
|
||||
}
|
||||
else -> Result.success(room.liveTimeline)
|
||||
else -> block(room.liveTimeline)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+40
-1
@@ -16,6 +16,7 @@ import io.element.android.libraries.matrix.api.media.FileInfo
|
||||
import io.element.android.libraries.matrix.api.media.ImageInfo
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.test.A_THREAD_ID
|
||||
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
|
||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
||||
@@ -156,14 +157,52 @@ class DefaultMediaSenderTest : RobolectricTest() {
|
||||
sendFileResult.assertions().isCalledOnce()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a thread, the created timeline is closed once the media is sent`() = runTest {
|
||||
val threadedTimeline = FakeTimeline(mode = Timeline.Mode.Thread(A_THREAD_ID)).apply {
|
||||
sendFileLambda = { _, _, _, _, _ -> Result.success(FakeMediaUploadHandler()) }
|
||||
}
|
||||
val liveTimeline = FakeTimeline()
|
||||
val room = FakeJoinedRoom(
|
||||
liveTimeline = liveTimeline,
|
||||
createTimelineResult = { Result.success(threadedTimeline) },
|
||||
)
|
||||
val sender = createDefaultMediaSender(
|
||||
room = room,
|
||||
timelineMode = Timeline.Mode.Thread(A_THREAD_ID),
|
||||
)
|
||||
|
||||
val uri = Uri.parse("content://image.jpg")
|
||||
val result = sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, mediaOptimizationConfig = mediaOptimizationConfig)
|
||||
|
||||
assertThat(result.isSuccess).isTrue()
|
||||
assertThat(threadedTimeline.closeCounter).isEqualTo(1)
|
||||
assertThat(liveTimeline.closeCounter).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a live timeline, the timeline is not closed once the media is sent`() = runTest {
|
||||
val liveTimeline = FakeTimeline().apply {
|
||||
sendFileLambda = { _, _, _, _, _ -> Result.success(FakeMediaUploadHandler()) }
|
||||
}
|
||||
val sender = createDefaultMediaSender(room = FakeJoinedRoom(liveTimeline = liveTimeline))
|
||||
|
||||
val uri = Uri.parse("content://image.jpg")
|
||||
val result = sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, mediaOptimizationConfig = mediaOptimizationConfig)
|
||||
|
||||
assertThat(result.isSuccess).isTrue()
|
||||
assertThat(liveTimeline.closeCounter).isEqualTo(0)
|
||||
}
|
||||
|
||||
private fun createDefaultMediaSender(
|
||||
preProcessor: MediaPreProcessor = FakeMediaPreProcessor(),
|
||||
room: JoinedRoom = FakeJoinedRoom(),
|
||||
timelineMode: Timeline.Mode = Timeline.Mode.Live,
|
||||
mediaOptimizationConfigProvider: MediaOptimizationConfigProvider = MediaOptimizationConfigProvider { mediaOptimizationConfig },
|
||||
) = DefaultMediaSender(
|
||||
preProcessor = preProcessor,
|
||||
room = room,
|
||||
timelineMode = Timeline.Mode.Live,
|
||||
timelineMode = timelineMode,
|
||||
mediaOptimizationConfigProvider = mediaOptimizationConfigProvider,
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user