diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt index eadbd544fc6..01ec68a3363 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt @@ -19,6 +19,7 @@ import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.core.extensions.mapCatchingExceptions import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MediaFile +import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint.MediaViewerMode import io.element.android.libraries.mediaviewer.api.local.LocalMedia @@ -40,6 +41,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext import timber.log.Timber +import java.util.concurrent.ConcurrentHashMap class MediaViewerDataSource( mode: MediaViewerMode, @@ -51,7 +53,7 @@ class MediaViewerDataSource( private val pagerKeysHandler: PagerKeysHandler, ) { // List of media files that are currently being loaded - private val mediaFiles: MutableList = mutableListOf() + private val mediaFiles: ConcurrentHashMap = ConcurrentHashMap() private val galleryMode = when (mode) { MediaViewerMode.SingleMedia, @@ -69,7 +71,7 @@ class MediaViewerDataSource( fun dispose() { Timber.d("Disposing MediaViewerDataSource, closing ${mediaFiles.size} media files") - mediaFiles.forEach { it.close() } + mediaFiles.values.forEach { it.close() } mediaFiles.clear() localMediaStates.clear() } @@ -163,6 +165,12 @@ class MediaViewerDataSource( } suspend fun loadMedia(data: MediaViewerPageData.MediaViewerData) { + val currentState = localMediaStates[data.mediaSource.safeUrl]?.value + // If the media is already loading or has been loaded successfully, do nothing + if (currentState?.isLoading() == true || currentState?.isSuccess() == true) { + return + } + Timber.d("loadMedia for ${data.eventId}") val localMediaState = localMediaStates.getOrPut(data.mediaSource.safeUrl) { mutableStateOf(AsyncData.Uninitialized) @@ -175,7 +183,7 @@ class MediaViewerDataSource( filename = data.mediaInfo.filename ) .onSuccess { mediaFile -> - mediaFiles.add(mediaFile) + mediaFiles[data.mediaSource] = mediaFile } .mapCatchingExceptions { mediaFile -> localMediaFactory.createFromMediaFile( @@ -190,4 +198,12 @@ class MediaViewerDataSource( localMediaState.value = AsyncData.Failure(it) } } + + fun cancelLoadingMedia(data: MediaViewerPageData.MediaViewerData) { + if (localMediaStates[data.mediaSource.safeUrl]?.value?.isLoading() == true) { + Timber.d("cancelLoadingMedia for ${data.eventId}") + mediaFiles.remove(data.mediaSource)?.close() + localMediaStates[data.mediaSource.safeUrl]?.value = AsyncData.Uninitialized + } + } } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerEvent.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerEvent.kt index 1960b69e4c0..53d287075df 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerEvent.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerEvent.kt @@ -29,4 +29,5 @@ sealed interface MediaViewerEvent { data class Delete(val eventId: EventId) : MediaViewerEvent data class OnNavigateTo(val index: Int) : MediaViewerEvent data class LoadMore(val direction: Timeline.PaginationDirection) : MediaViewerEvent + data class CancelLoadingMedia(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvent } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt index 138c73c383a..d40026dd375 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt @@ -100,6 +100,9 @@ class MediaViewerPresenter( is MediaViewerEvent.LoadMedia -> { coroutineScope.downloadMedia(data = event.data) } + is MediaViewerEvent.CancelLoadingMedia -> { + dataSource.cancelLoadingMedia(event.data) + } is MediaViewerEvent.ClearLoadingError -> { dataSource.clearLoadingError(event.data) } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt index 8bc50881874..5e9ced7d77e 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt @@ -52,6 +52,7 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.layout.onVisibilityChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.stringResource @@ -208,11 +209,16 @@ fun MediaViewerView( } is MediaViewerPageData.MediaViewerData -> { var bottomPaddingInPixels by remember { mutableIntStateOf(defaultBottomPaddingInPixels) } - LaunchedEffect(Unit) { - state.eventSink(MediaViewerEvent.LoadMedia(dataForPage)) - } Box( - modifier = Modifier.fillMaxSize() + modifier = Modifier + .onVisibilityChanged(minDurationMs = 200L) { isVisible -> + if (isVisible) { + state.eventSink(MediaViewerEvent.LoadMedia(dataForPage)) + } else { + state.eventSink(MediaViewerEvent.CancelLoadingMedia(dataForPage)) + } + } + .fillMaxSize() ) { val isDisplayed = remember(pagerState.settledPage) { // This 'item provider' lambda will be called when the data source changes with an outdated `settlePage` value diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerViewTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerViewTest.kt index fdd447c4a67..b426ca50ca0 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerViewTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerViewTest.kt @@ -52,6 +52,10 @@ class MediaViewerViewTest { state = state, onBackClick = callback, ) + + // Wait for enough time for the onVisibilityChanged modifier to trigger + mainClock.advanceTimeBy(200) + pressBack() } eventsRecorder.assertList( @@ -110,6 +114,10 @@ class MediaViewerViewTest { eventSink = eventsRecorder ), ) + + // Wait for enough time for the onVisibilityChanged modifier to trigger + mainClock.advanceTimeBy(200) + val contentDescription = activity!!.getString(contentDescriptionRes) onNodeWithContentDescription(contentDescription).performClick() eventsRecorder.assertList( @@ -241,6 +249,10 @@ class MediaViewerViewTest { eventSink = eventsRecorder ), ) + + // Wait for enough time for the onVisibilityChanged modifier to trigger + mainClock.advanceTimeBy(200) + clickOn(CommonStrings.action_retry) eventsRecorder.assertList( listOf( @@ -263,6 +275,10 @@ class MediaViewerViewTest { eventSink = eventsRecorder ), ) + + // Wait for enough time for the onVisibilityChanged modifier to trigger + mainClock.advanceTimeBy(200) + clickOn(CommonStrings.action_cancel) eventsRecorder.assertList( listOf(