Merge pull request #5722 from element-hq/feature/bma/moduleCleanup
Module cleanup
This commit is contained in:
@@ -64,6 +64,7 @@ dependencies {
|
||||
testImplementation(projects.features.forward.test)
|
||||
testImplementation(projects.features.networkmonitor.test)
|
||||
testImplementation(projects.features.rageshake.test)
|
||||
testImplementation(projects.services.appnavstate.impl)
|
||||
testImplementation(projects.services.appnavstate.test)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
testImplementation(projects.services.toolbox.test)
|
||||
|
||||
@@ -35,6 +35,7 @@ import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||
import io.element.android.services.appnavstate.impl.DefaultActiveRoomsHolder
|
||||
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
@@ -108,7 +109,7 @@ class JoinedRoomLoadedFlowNodeTest {
|
||||
roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(),
|
||||
spaceEntryPoint: SpaceEntryPoint = FakeSpaceEntryPoint(),
|
||||
forwardEntryPoint: ForwardEntryPoint = FakeForwardEntryPoint(),
|
||||
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
|
||||
activeRoomsHolder: ActiveRoomsHolder = DefaultActiveRoomsHolder(),
|
||||
matrixClient: FakeMatrixClient = FakeMatrixClient(),
|
||||
) = JoinedRoomLoadedFlowNode(
|
||||
buildContext = BuildContext.root(savedStateMap = null),
|
||||
@@ -192,7 +193,7 @@ class JoinedRoomLoadedFlowNodeTest {
|
||||
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
||||
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
|
||||
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Root())
|
||||
val activeRoomsHolder = ActiveRoomsHolder()
|
||||
val activeRoomsHolder = DefaultActiveRoomsHolder()
|
||||
val roomFlowNode = createJoinedRoomLoadedFlowNode(
|
||||
plugins = listOf(inputs, FakeJoinedRoomLoadedFlowNodeCallback()),
|
||||
messagesEntryPoint = fakeMessagesEntryPoint,
|
||||
@@ -215,7 +216,7 @@ class JoinedRoomLoadedFlowNodeTest {
|
||||
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
||||
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
|
||||
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Root())
|
||||
val activeRoomsHolder = ActiveRoomsHolder().apply {
|
||||
val activeRoomsHolder = DefaultActiveRoomsHolder().apply {
|
||||
addRoom(room)
|
||||
}
|
||||
val roomFlowNode = createJoinedRoomLoadedFlowNode(
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import extension.setupDependencyInjection
|
||||
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
@@ -16,8 +14,6 @@ android {
|
||||
namespace = "io.element.android.features.cachecleaner.api"
|
||||
}
|
||||
|
||||
setupDependencyInjection()
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(libs.androidx.startup)
|
||||
|
||||
@@ -97,6 +97,7 @@ dependencies {
|
||||
testImplementation(projects.libraries.matrixmedia.test)
|
||||
testImplementation(projects.libraries.push.test)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
testImplementation(projects.services.appnavstate.impl)
|
||||
testImplementation(projects.services.appnavstate.test)
|
||||
testImplementation(projects.services.toolbox.test)
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
|
||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
|
||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||
import io.element.android.services.appnavstate.impl.DefaultActiveRoomsHolder
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
@@ -86,7 +87,7 @@ class DefaultCallWidgetProviderTest {
|
||||
// No room from the client
|
||||
givenGetRoomResult(A_ROOM_ID, null)
|
||||
}
|
||||
val activeRoomsHolder = ActiveRoomsHolder().apply {
|
||||
val activeRoomsHolder = DefaultActiveRoomsHolder().apply {
|
||||
// A current active room with the same room id
|
||||
addRoom(
|
||||
FakeJoinedRoom(
|
||||
@@ -130,7 +131,7 @@ class DefaultCallWidgetProviderTest {
|
||||
matrixClientProvider: MatrixClientProvider = FakeMatrixClientProvider(),
|
||||
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
|
||||
callWidgetSettingsProvider: CallWidgetSettingsProvider = FakeCallWidgetSettingsProvider(),
|
||||
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
|
||||
activeRoomsHolder: ActiveRoomsHolder = DefaultActiveRoomsHolder(),
|
||||
) = DefaultCallWidgetProvider(
|
||||
matrixClientsProvider = matrixClientProvider,
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
|
||||
@@ -90,6 +90,7 @@ dependencies {
|
||||
testImplementation(projects.services.analytics.test)
|
||||
testImplementation(projects.services.toolbox.test)
|
||||
testImplementation(projects.libraries.featureflag.test)
|
||||
testImplementation(projects.libraries.mediaupload.impl)
|
||||
testImplementation(projects.libraries.mediaupload.test)
|
||||
testImplementation(projects.libraries.mediapickers.test)
|
||||
testImplementation(projects.libraries.permissions.test)
|
||||
|
||||
@@ -37,7 +37,7 @@ import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfig
|
||||
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||
import io.element.android.libraries.mediaupload.api.MediaSenderFactory
|
||||
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
||||
import io.element.android.libraries.mediaupload.api.allFiles
|
||||
import io.element.android.libraries.preferences.api.store.VideoCompressionPreset
|
||||
@@ -56,7 +56,7 @@ class AttachmentsPreviewPresenter(
|
||||
@Assisted private val onDoneListener: OnDoneListener,
|
||||
@Assisted private val timelineMode: Timeline.Mode,
|
||||
@Assisted private val inReplyToEventId: EventId?,
|
||||
mediaSenderFactory: MediaSender.Factory,
|
||||
mediaSenderFactory: MediaSenderFactory,
|
||||
private val permalinkBuilder: PermalinkBuilder,
|
||||
private val temporaryUriDeleter: TemporaryUriDeleter,
|
||||
private val mediaOptimizationSelectorPresenterFactory: MediaOptimizationSelectorPresenter.Factory,
|
||||
|
||||
@@ -61,7 +61,7 @@ import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
|
||||
import io.element.android.libraries.matrix.ui.messages.reply.map
|
||||
import io.element.android.libraries.mediapickers.api.PickerProvider
|
||||
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider
|
||||
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||
import io.element.android.libraries.mediaupload.api.MediaSenderFactory
|
||||
import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory
|
||||
import io.element.android.libraries.permissions.api.PermissionsEvents
|
||||
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||
@@ -107,7 +107,7 @@ class MessageComposerPresenter(
|
||||
private val mediaPickerProvider: PickerProvider,
|
||||
private val sessionPreferencesStore: SessionPreferencesStore,
|
||||
private val localMediaFactory: LocalMediaFactory,
|
||||
private val mediaSenderFactory: MediaSender.Factory,
|
||||
mediaSenderFactory: MediaSenderFactory,
|
||||
private val snackbarDispatcher: SnackbarDispatcher,
|
||||
private val analyticsService: AnalyticsService,
|
||||
private val locationService: LocationService,
|
||||
|
||||
@@ -32,7 +32,7 @@ import io.element.android.features.messages.api.timeline.voicemessages.composer.
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||
import io.element.android.libraries.mediaupload.api.MediaSenderFactory
|
||||
import io.element.android.libraries.permissions.api.PermissionsEvents
|
||||
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||
import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent
|
||||
@@ -57,7 +57,7 @@ class DefaultVoiceMessageComposerPresenter(
|
||||
@Assisted private val timelineMode: Timeline.Mode,
|
||||
private val voiceRecorder: VoiceRecorder,
|
||||
private val analyticsService: AnalyticsService,
|
||||
mediaSenderFactory: MediaSender.Factory,
|
||||
mediaSenderFactory: MediaSenderFactory,
|
||||
private val player: VoiceMessageComposerPlayer,
|
||||
private val messageComposerContext: MessageComposerContext,
|
||||
permissionsPresenterFactory: PermissionsPresenter.Factory
|
||||
|
||||
@@ -78,7 +78,6 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_THREAD_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
||||
@@ -166,6 +165,7 @@ class MessagesPresenterTest {
|
||||
val toggleReactionSuccess = lambdaRecorder { _: String, _: EventOrTransactionId -> Result.success(true) }
|
||||
val toggleReactionFailure =
|
||||
lambdaRecorder { _: String, _: EventOrTransactionId -> Result.failure<Boolean>(IllegalStateException("Failed to send reaction")) }
|
||||
val addRecentEmojiResult = lambdaRecorder { _: String -> Result.success(Unit) }
|
||||
|
||||
val timeline = FakeTimeline().apply {
|
||||
this.toggleReactionLambda = toggleReactionSuccess
|
||||
@@ -184,7 +184,8 @@ class MessagesPresenterTest {
|
||||
val presenter = createMessagesPresenter(
|
||||
timeline = timeline,
|
||||
joinedRoom = room,
|
||||
coroutineDispatchers = coroutineDispatchers
|
||||
addRecentEmoji = AddRecentEmoji { addRecentEmojiResult(it) },
|
||||
coroutineDispatchers = coroutineDispatchers,
|
||||
)
|
||||
presenter.testWithLifecycleOwner {
|
||||
skipItems(1)
|
||||
@@ -201,6 +202,7 @@ class MessagesPresenterTest {
|
||||
assert(toggleReactionFailure)
|
||||
.isCalledOnce()
|
||||
.with(value("👍"), value(AN_EVENT_ID.toEventOrTransactionId()))
|
||||
addRecentEmojiResult.assertions().isCalledOnce().with(value("👍"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,7 +214,9 @@ class MessagesPresenterTest {
|
||||
toggle = !toggle
|
||||
Result.success(toggle)
|
||||
}
|
||||
|
||||
val addRecentEmoji = lambdaRecorder { _: String ->
|
||||
Result.success(Unit)
|
||||
}
|
||||
val timeline = FakeTimeline().apply {
|
||||
this.toggleReactionLambda = toggleReactionSuccess
|
||||
}
|
||||
@@ -230,6 +234,7 @@ class MessagesPresenterTest {
|
||||
val presenter = createMessagesPresenter(
|
||||
timeline = timeline,
|
||||
joinedRoom = room,
|
||||
addRecentEmoji = AddRecentEmoji { addRecentEmoji(it) },
|
||||
coroutineDispatchers = coroutineDispatchers
|
||||
)
|
||||
presenter.testWithLifecycleOwner {
|
||||
@@ -244,6 +249,7 @@ class MessagesPresenterTest {
|
||||
listOf(value("👍"), value(AN_EVENT_ID.toEventOrTransactionId())),
|
||||
)
|
||||
skipItems(1)
|
||||
addRecentEmoji.assertions().isCalledOnce().with(value("👍"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1196,10 +1202,12 @@ class MessagesPresenterTest {
|
||||
)
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(MessagesEvents.HandleAction(
|
||||
action = TimelineItemAction.ReplyInThread,
|
||||
event = aMessageEvent(threadInfo = TimelineItemThreadInfo.ThreadResponse(A_THREAD_ID))
|
||||
))
|
||||
initialState.eventSink(
|
||||
MessagesEvents.HandleAction(
|
||||
action = TimelineItemAction.ReplyInThread,
|
||||
event = aMessageEvent(threadInfo = TimelineItemThreadInfo.ThreadResponse(A_THREAD_ID))
|
||||
)
|
||||
)
|
||||
awaitItem()
|
||||
openThreadLambda.assertions().isCalledOnce().with(value(A_THREAD_ID), value(null))
|
||||
}
|
||||
@@ -1216,14 +1224,16 @@ class MessagesPresenterTest {
|
||||
)
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(MessagesEvents.HandleAction(
|
||||
action = TimelineItemAction.ReplyInThread,
|
||||
event = aMessageEvent(
|
||||
// The event id will be used as the thread id instead
|
||||
eventId = AN_EVENT_ID,
|
||||
threadInfo = null,
|
||||
initialState.eventSink(
|
||||
MessagesEvents.HandleAction(
|
||||
action = TimelineItemAction.ReplyInThread,
|
||||
event = aMessageEvent(
|
||||
// The event id will be used as the thread id instead
|
||||
eventId = AN_EVENT_ID,
|
||||
threadInfo = null,
|
||||
)
|
||||
)
|
||||
))
|
||||
)
|
||||
awaitItem()
|
||||
openThreadLambda.assertions().isCalledOnce().with(value(AN_EVENT_ID.toThreadId()), value(null))
|
||||
}
|
||||
@@ -1334,7 +1344,7 @@ class MessagesPresenterTest {
|
||||
encryptionService: FakeEncryptionService = FakeEncryptionService(),
|
||||
featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(),
|
||||
actionListEventSink: (ActionListEvents) -> Unit = {},
|
||||
addRecentEmoji: AddRecentEmoji = AddRecentEmoji(FakeMatrixClient(), testCoroutineDispatchers()),
|
||||
addRecentEmoji: AddRecentEmoji = AddRecentEmoji { _ -> lambdaError() },
|
||||
markAsFullyRead: MarkAsFullyRead = FakeMarkAsFullyRead(),
|
||||
): MessagesPresenter {
|
||||
return MessagesPresenter(
|
||||
|
||||
@@ -41,8 +41,9 @@ import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
||||
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfig
|
||||
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
||||
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||
import io.element.android.libraries.mediaupload.api.MediaSenderFactory
|
||||
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
||||
import io.element.android.libraries.mediaupload.impl.DefaultMediaSender
|
||||
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
||||
import io.element.android.libraries.mediaviewer.api.aVideoMediaInfo
|
||||
import io.element.android.libraries.mediaviewer.api.anApkMediaInfo
|
||||
@@ -601,17 +602,15 @@ class AttachmentsPreviewPresenterTest {
|
||||
return AttachmentsPreviewPresenter(
|
||||
attachment = aMediaAttachment(localMedia),
|
||||
onDoneListener = onDoneListener,
|
||||
mediaSenderFactory = object : MediaSender.Factory {
|
||||
override fun create(timelineMode: Timeline.Mode): MediaSender {
|
||||
return MediaSender(
|
||||
preProcessor = mediaPreProcessor,
|
||||
room = room,
|
||||
timelineMode = timelineMode,
|
||||
mediaOptimizationConfigProvider = {
|
||||
MediaOptimizationConfig(compressImages = true, videoCompressionPreset = VideoCompressionPreset.STANDARD)
|
||||
}
|
||||
)
|
||||
}
|
||||
mediaSenderFactory = MediaSenderFactory { timelineMode ->
|
||||
DefaultMediaSender(
|
||||
preProcessor = mediaPreProcessor,
|
||||
room = room,
|
||||
timelineMode = timelineMode,
|
||||
mediaOptimizationConfigProvider = {
|
||||
MediaOptimizationConfig(compressImages = true, videoCompressionPreset = VideoCompressionPreset.STANDARD)
|
||||
}
|
||||
)
|
||||
},
|
||||
permalinkBuilder = permalinkBuilder,
|
||||
temporaryUriDeleter = temporaryUriDeleter,
|
||||
|
||||
@@ -21,7 +21,6 @@ import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.mediaupload.api.MaxUploadSizeProvider
|
||||
import io.element.android.libraries.mediaviewer.api.aVideoMediaInfo
|
||||
import io.element.android.libraries.mediaviewer.api.anImageMediaInfo
|
||||
@@ -206,7 +205,7 @@ class DefaultMediaOptimizationSelectorPresenterTest {
|
||||
@Test
|
||||
fun `present - max upload size will default to 100MB if we can't get it`() = runTest {
|
||||
val presenter = createDefaultMediaOptimizationSelectorPresenter(
|
||||
maxUploadSizeProvider = MaxUploadSizeProvider(FakeMatrixClient(getMaxUploadSizeResult = { Result.failure(AN_EXCEPTION) }))
|
||||
maxUploadSizeProvider = MaxUploadSizeProvider { Result.failure(AN_EXCEPTION) }
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
@@ -233,9 +232,7 @@ class DefaultMediaOptimizationSelectorPresenterTest {
|
||||
|
||||
private fun createDefaultMediaOptimizationSelectorPresenter(
|
||||
localMedia: LocalMedia = aLocalMedia(mockMediaUrl, aVideoMediaInfo()),
|
||||
maxUploadSizeProvider: MaxUploadSizeProvider = MaxUploadSizeProvider(
|
||||
FakeMatrixClient(getMaxUploadSizeResult = { Result.success(1_000L) }),
|
||||
),
|
||||
maxUploadSizeProvider: MaxUploadSizeProvider = MaxUploadSizeProvider { Result.success(1_000L) },
|
||||
sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(),
|
||||
featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.SelectableMediaQuality.key to true)),
|
||||
mediaExtractorFactory: FakeVideoMetadataExtractorFactory = FakeVideoMetadataExtractorFactory(),
|
||||
|
||||
@@ -75,8 +75,9 @@ import io.element.android.libraries.mediapickers.api.PickerProvider
|
||||
import io.element.android.libraries.mediapickers.test.FakePickerProvider
|
||||
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfig
|
||||
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
||||
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||
import io.element.android.libraries.mediaupload.api.MediaSenderFactory
|
||||
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
||||
import io.element.android.libraries.mediaupload.impl.DefaultMediaSender
|
||||
import io.element.android.libraries.mediaupload.test.FakeMediaOptimizationConfigProvider
|
||||
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
||||
import io.element.android.libraries.mediaviewer.test.FakeLocalMediaFactory
|
||||
@@ -1551,20 +1552,18 @@ class MessageComposerPresenterTest {
|
||||
mediaPickerProvider = pickerProvider,
|
||||
sessionPreferencesStore = sessionPreferencesStore,
|
||||
localMediaFactory = localMediaFactory,
|
||||
mediaSenderFactory = object : MediaSender.Factory {
|
||||
override fun create(timelineMode: Timeline.Mode): MediaSender {
|
||||
return MediaSender(
|
||||
preProcessor = mediaPreProcessor,
|
||||
room = room,
|
||||
timelineMode = timelineMode,
|
||||
mediaOptimizationConfigProvider = {
|
||||
MediaOptimizationConfig(
|
||||
mediaSenderFactory = MediaSenderFactory { timelineMode ->
|
||||
DefaultMediaSender(
|
||||
preProcessor = mediaPreProcessor,
|
||||
room = room,
|
||||
timelineMode = timelineMode,
|
||||
mediaOptimizationConfigProvider = {
|
||||
MediaOptimizationConfig(
|
||||
compressImages = true,
|
||||
videoCompressionPreset = VideoCompressionPreset.STANDARD
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
snackbarDispatcher = snackbarDispatcher,
|
||||
analyticsService = analyticsService,
|
||||
|
||||
@@ -30,7 +30,7 @@ import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
||||
import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer
|
||||
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfig
|
||||
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||
import io.element.android.libraries.mediaupload.impl.DefaultMediaSender
|
||||
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
||||
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||
import io.element.android.libraries.permissions.api.aPermissionsState
|
||||
@@ -75,7 +75,7 @@ class VoiceMessageComposerPresenterTest {
|
||||
},
|
||||
)
|
||||
private val mediaPreProcessor = FakeMediaPreProcessor().apply { givenAudioResult() }
|
||||
private val mediaSender = MediaSender(
|
||||
private val mediaSender = DefaultMediaSender(
|
||||
preProcessor = mediaPreProcessor,
|
||||
room = joinedRoom,
|
||||
timelineMode = Timeline.Mode.Live,
|
||||
@@ -668,11 +668,7 @@ class VoiceMessageComposerPresenterTest {
|
||||
timelineMode = Timeline.Mode.Live,
|
||||
voiceRecorder = voiceRecorder,
|
||||
analyticsService = analyticsService,
|
||||
mediaSenderFactory = object : MediaSender.Factory {
|
||||
override fun create(timelineMode: Timeline.Mode): MediaSender {
|
||||
return mediaSender
|
||||
}
|
||||
},
|
||||
mediaSenderFactory = { mediaSender },
|
||||
player = VoiceMessageComposerPlayer(FakeMediaPlayer(), this),
|
||||
messageComposerContext = messageComposerContext,
|
||||
permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter),
|
||||
|
||||
@@ -25,4 +25,5 @@ dependencies {
|
||||
implementation(projects.libraries.voicerecorder.test)
|
||||
implementation(projects.services.analytics.test)
|
||||
implementation(projects.tests.testutils)
|
||||
implementation(projects.libraries.mediaupload.impl)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||
import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer
|
||||
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||
import io.element.android.libraries.mediaupload.impl.DefaultMediaSender
|
||||
import io.element.android.libraries.mediaupload.test.FakeMediaOptimizationConfigProvider
|
||||
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
||||
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
|
||||
@@ -24,7 +25,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class FakeDefaultVoiceMessageComposerPresenterFactory(
|
||||
private val sessionCoroutineScope: CoroutineScope,
|
||||
private val mediaSender: MediaSender = MediaSender(
|
||||
private val mediaSender: MediaSender = DefaultMediaSender(
|
||||
preProcessor = FakeMediaPreProcessor(),
|
||||
room = FakeJoinedRoom(),
|
||||
timelineMode = Timeline.Mode.Live,
|
||||
@@ -37,11 +38,7 @@ class FakeDefaultVoiceMessageComposerPresenterFactory(
|
||||
timelineMode = timelineMode,
|
||||
voiceRecorder = FakeVoiceRecorder(),
|
||||
analyticsService = FakeAnalyticsService(),
|
||||
mediaSenderFactory = object : MediaSender.Factory {
|
||||
override fun create(timelineMode: Timeline.Mode): MediaSender {
|
||||
return mediaSender
|
||||
}
|
||||
},
|
||||
mediaSenderFactory = { mediaSender },
|
||||
player = VoiceMessageComposerPlayer(
|
||||
mediaPlayer = FakeMediaPlayer(),
|
||||
sessionCoroutineScope = sessionCoroutineScope,
|
||||
|
||||
@@ -115,6 +115,7 @@ dependencies {
|
||||
testImplementation(projects.libraries.indicator.test)
|
||||
testImplementation(projects.libraries.pushproviders.test)
|
||||
testImplementation(projects.libraries.sessionStorage.test)
|
||||
testImplementation(projects.services.appnavstate.impl)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
testImplementation(projects.services.toolbox.test)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||
import io.element.android.libraries.push.test.FakePushService
|
||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||
import io.element.android.services.appnavstate.impl.DefaultActiveRoomsHolder
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
@@ -34,7 +34,7 @@ import org.robolectric.RobolectricTestRunner
|
||||
class DefaultClearCacheUseCaseTest {
|
||||
@Test
|
||||
fun `execute clear cache should do all the expected tasks`() = runTest {
|
||||
val activeRoomsHolder = ActiveRoomsHolder().apply { addRoom(FakeJoinedRoom()) }
|
||||
val activeRoomsHolder = DefaultActiveRoomsHolder().apply { addRoom(FakeJoinedRoom()) }
|
||||
val clearCacheLambda = lambdaRecorder<Unit> { }
|
||||
val matrixClient = FakeMatrixClient(
|
||||
sessionId = A_SESSION_ID,
|
||||
|
||||
@@ -48,4 +48,5 @@ dependencies {
|
||||
testImplementation(projects.libraries.mediaupload.test)
|
||||
testImplementation(projects.libraries.preferences.test)
|
||||
testImplementation(projects.libraries.roomselect.test)
|
||||
testImplementation(projects.services.appnavstate.impl)
|
||||
}
|
||||
|
||||
@@ -23,10 +23,8 @@ import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider
|
||||
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
||||
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||
import io.element.android.libraries.mediaupload.api.MediaSenderRoomFactory
|
||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -39,7 +37,7 @@ class SharePresenter(
|
||||
private val sessionCoroutineScope: CoroutineScope,
|
||||
private val shareIntentHandler: ShareIntentHandler,
|
||||
private val matrixClient: MatrixClient,
|
||||
private val mediaPreProcessor: MediaPreProcessor,
|
||||
private val mediaSenderRoomFactory: MediaSenderRoomFactory,
|
||||
private val activeRoomsHolder: ActiveRoomsHolder,
|
||||
private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider,
|
||||
) : Presenter<ShareState> {
|
||||
@@ -88,12 +86,7 @@ class SharePresenter(
|
||||
roomIds
|
||||
.map { roomId ->
|
||||
val room = getJoinedRoom(roomId) ?: return@map false
|
||||
val mediaSender = MediaSender(
|
||||
preProcessor = mediaPreProcessor,
|
||||
room = room,
|
||||
timelineMode = Timeline.Mode.Live,
|
||||
mediaOptimizationConfigProvider = mediaOptimizationConfigProvider,
|
||||
)
|
||||
val mediaSender = mediaSenderRoomFactory.create(room = room)
|
||||
filesToShare
|
||||
.map { fileToShare ->
|
||||
val result = mediaSender.sendMedia(
|
||||
|
||||
@@ -17,18 +17,17 @@ import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.media.FileInfo
|
||||
import io.element.android.libraries.matrix.test.A_MESSAGE
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
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
|
||||
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
||||
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider
|
||||
import io.element.android.libraries.mediaupload.api.MediaSenderRoomFactory
|
||||
import io.element.android.libraries.mediaupload.test.FakeMediaOptimizationConfigProvider
|
||||
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
||||
import io.element.android.libraries.mediaupload.test.FakeMediaSender
|
||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||
import io.element.android.services.appnavstate.impl.DefaultActiveRoomsHolder
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
@@ -37,7 +36,6 @@ import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import java.io.File
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class SharePresenterTest {
|
||||
@@ -121,18 +119,16 @@ class SharePresenterTest {
|
||||
|
||||
@Test
|
||||
fun `present - send media ok`() = runTest {
|
||||
val sendFileResult =
|
||||
lambdaRecorder<File, FileInfo, String?, String?, EventId?, Result<FakeMediaUploadHandler>> { _, _, _, _, _ ->
|
||||
Result.success(FakeMediaUploadHandler())
|
||||
}
|
||||
val sendMediaResult = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
|
||||
val joinedRoom = FakeJoinedRoom(
|
||||
liveTimeline = FakeTimeline().apply {
|
||||
sendFileLambda = sendFileResult
|
||||
},
|
||||
liveTimeline = FakeTimeline(),
|
||||
)
|
||||
val matrixClient = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(A_ROOM_ID, joinedRoom)
|
||||
}
|
||||
val mediaSender = FakeMediaSender(
|
||||
sendMediaResult = sendMediaResult,
|
||||
)
|
||||
val presenter = createSharePresenter(
|
||||
matrixClient = matrixClient,
|
||||
shareIntentHandler = FakeShareIntentHandler { _, onFile, _ ->
|
||||
@@ -144,7 +140,8 @@ class SharePresenterTest {
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
mediaSenderRoomFactory = MediaSenderRoomFactory { mediaSender },
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
@@ -156,7 +153,7 @@ class SharePresenterTest {
|
||||
val success = awaitItem()
|
||||
assertThat(success.shareAction.isSuccess()).isTrue()
|
||||
assertThat(success.shareAction).isEqualTo(AsyncAction.Success(listOf(A_ROOM_ID)))
|
||||
sendFileResult.assertions().isCalledOnce()
|
||||
sendMediaResult.assertions().isCalledOnce()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,17 +162,17 @@ internal fun TestScope.createSharePresenter(
|
||||
intent: Intent = Intent(),
|
||||
shareIntentHandler: ShareIntentHandler = FakeShareIntentHandler(),
|
||||
matrixClient: MatrixClient = FakeMatrixClient(),
|
||||
mediaPreProcessor: MediaPreProcessor = FakeMediaPreProcessor(),
|
||||
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
|
||||
mediaOptimizationConfigProvider: FakeMediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(),
|
||||
activeRoomsHolder: ActiveRoomsHolder = DefaultActiveRoomsHolder(),
|
||||
mediaSenderRoomFactory: MediaSenderRoomFactory = MediaSenderRoomFactory { FakeMediaSender() },
|
||||
mediaOptimizationConfigProvider: MediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(),
|
||||
): SharePresenter {
|
||||
return SharePresenter(
|
||||
intent = intent,
|
||||
sessionCoroutineScope = this,
|
||||
shareIntentHandler = shareIntentHandler,
|
||||
matrixClient = matrixClient,
|
||||
mediaPreProcessor = mediaPreProcessor,
|
||||
activeRoomsHolder = activeRoomsHolder,
|
||||
mediaSenderRoomFactory = mediaSenderRoomFactory,
|
||||
mediaOptimizationConfigProvider = mediaOptimizationConfigProvider,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import config.BuildTimeConfig
|
||||
import extension.buildConfigFieldStr
|
||||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
@@ -17,8 +16,6 @@ plugins {
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
}
|
||||
|
||||
setupDependencyInjection()
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.matrix.api"
|
||||
|
||||
|
||||
@@ -8,28 +8,12 @@
|
||||
|
||||
package io.element.android.libraries.matrix.api.mxc
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
|
||||
@Inject
|
||||
class MxcTools {
|
||||
/**
|
||||
* Regex to match a Matrix Content (mxc://) URI.
|
||||
*
|
||||
* See: https://spec.matrix.org/v1.8/client-server-api/#matrix-content-mxc-uris
|
||||
*/
|
||||
private val mxcRegex = Regex("""^mxc://([^/]+)/([^/]+)$""")
|
||||
|
||||
interface MxcTools {
|
||||
/**
|
||||
* Sanitizes an mxcUri to be used as a relative file path.
|
||||
*
|
||||
* @param mxcUri the Matrix Content (mxc://) URI of the file.
|
||||
* @return the relative file path as "<server-name>/<media-id>" or null if the mxcUri is invalid.
|
||||
*/
|
||||
fun mxcUri2FilePath(mxcUri: String): String? = mxcRegex.matchEntire(mxcUri)?.let { match ->
|
||||
buildString {
|
||||
append(match.groupValues[1])
|
||||
append("/")
|
||||
append(match.groupValues[2])
|
||||
}
|
||||
}
|
||||
fun mxcUri2FilePath(mxcUri: String): String?
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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.libraries.matrix.impl.mxc
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.matrix.api.mxc.MxcTools
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultMxcTools : MxcTools {
|
||||
/**
|
||||
* Regex to match a Matrix Content (mxc://) URI.
|
||||
*
|
||||
* See: https://spec.matrix.org/v1.8/client-server-api/#matrix-content-mxc-uris
|
||||
*/
|
||||
private val mxcRegex = Regex("""^mxc://([^/]+)/([^/]+)$""")
|
||||
|
||||
/**
|
||||
* Sanitizes an mxcUri to be used as a relative file path.
|
||||
*
|
||||
* @param mxcUri the Matrix Content (mxc://) URI of the file.
|
||||
* @return the relative file path as "<server-name>/<media-id>" or null if the mxcUri is invalid.
|
||||
*/
|
||||
override fun mxcUri2FilePath(mxcUri: String): String? = mxcRegex.matchEntire(mxcUri)?.let { match ->
|
||||
buildString {
|
||||
append(match.groupValues[1])
|
||||
append("/")
|
||||
append(match.groupValues[2])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,15 +6,15 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.api.mxc
|
||||
package io.element.android.libraries.matrix.impl.mxc
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Test
|
||||
|
||||
class MxcToolsTest {
|
||||
class DefaultMxcToolsTest {
|
||||
@Test
|
||||
fun `mxcUri2FilePath returns extracted path`() {
|
||||
val mxcTools = MxcTools()
|
||||
val mxcTools = DefaultMxcTools()
|
||||
val mxcUri = "mxc://server.org/abc123"
|
||||
val filePath = mxcTools.mxcUri2FilePath(mxcUri)
|
||||
assertThat(filePath).isEqualTo("server.org/abc123")
|
||||
@@ -22,7 +22,7 @@ class MxcToolsTest {
|
||||
|
||||
@Test
|
||||
fun `mxcUri2FilePath returns null for invalid data`() {
|
||||
val mxcTools = MxcTools()
|
||||
val mxcTools = DefaultMxcTools()
|
||||
assertThat(mxcTools.mxcUri2FilePath("")).isNull()
|
||||
assertThat(mxcTools.mxcUri2FilePath("mxc://server.org")).isNull()
|
||||
assertThat(mxcTools.mxcUri2FilePath("mxc://server.org/")).isNull()
|
||||
@@ -19,6 +19,7 @@ dependencies {
|
||||
api(projects.libraries.matrix.api)
|
||||
api(libs.coroutines.core)
|
||||
implementation(libs.coroutines.test)
|
||||
implementation(projects.libraries.matrix.impl)
|
||||
implementation(projects.services.analytics.api)
|
||||
implementation(projects.tests.testutils)
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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.libraries.matrix.test.mxc
|
||||
|
||||
import io.element.android.libraries.matrix.api.mxc.MxcTools
|
||||
import io.element.android.libraries.matrix.impl.mxc.DefaultMxcTools
|
||||
|
||||
class FakeMxcTools(
|
||||
private val delegate: MxcTools = DefaultMxcTools()
|
||||
) : MxcTools by delegate
|
||||
@@ -1,5 +1,3 @@
|
||||
import extension.setupDependencyInjection
|
||||
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
@@ -12,8 +10,6 @@ plugins {
|
||||
id("io.element.android-compose-library")
|
||||
}
|
||||
|
||||
setupDependencyInjection()
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.mediapickers.test"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
@@ -13,8 +12,6 @@ plugins {
|
||||
id("io.element.android-library")
|
||||
}
|
||||
|
||||
setupDependencyInjection()
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.mediaupload.api"
|
||||
}
|
||||
@@ -27,9 +24,4 @@ dependencies {
|
||||
api(projects.libraries.matrix.api)
|
||||
api(projects.libraries.preferences.api)
|
||||
implementation(libs.coroutines.core)
|
||||
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.preferences.test)
|
||||
testImplementation(projects.libraries.mediaupload.test)
|
||||
}
|
||||
|
||||
@@ -8,17 +8,9 @@
|
||||
|
||||
package io.element.android.libraries.mediaupload.api
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
|
||||
/**
|
||||
* Provides the maximum upload size allowed by the Matrix server.
|
||||
*/
|
||||
@Inject
|
||||
class MaxUploadSizeProvider(
|
||||
private val matrixClient: MatrixClient,
|
||||
) {
|
||||
suspend fun getMaxUploadSize(): Result<Long> {
|
||||
return matrixClient.getMaxFileUploadSize()
|
||||
}
|
||||
fun interface MaxUploadSizeProvider {
|
||||
suspend fun getMaxUploadSize(): Result<Long>
|
||||
}
|
||||
|
||||
@@ -9,73 +9,41 @@
|
||||
package io.element.android.libraries.mediaupload.api
|
||||
|
||||
import android.net.Uri
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.libraries.androidutils.hash.hash
|
||||
import io.element.android.libraries.core.extensions.flatMap
|
||||
import io.element.android.libraries.core.extensions.flatMapCatching
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
|
||||
import io.element.android.libraries.matrix.api.room.CreateTimelineParams
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Job
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
@AssistedInject
|
||||
class MediaSender(
|
||||
private val preProcessor: MediaPreProcessor,
|
||||
private val room: JoinedRoom,
|
||||
@Assisted private val timelineMode: Timeline.Mode,
|
||||
private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider,
|
||||
) {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(
|
||||
timelineMode: Timeline.Mode,
|
||||
): MediaSender
|
||||
}
|
||||
fun interface MediaSenderFactory {
|
||||
/**
|
||||
* Create a [MediaSender] for the given [Timeline.Mode], in the Room Scope.
|
||||
*/
|
||||
fun create(
|
||||
timelineMode: Timeline.Mode,
|
||||
): MediaSender
|
||||
}
|
||||
|
||||
private val ongoingUploadJobs = ConcurrentHashMap<Job.Key, MediaUploadHandler>()
|
||||
val hasOngoingMediaUploads get() = ongoingUploadJobs.isNotEmpty()
|
||||
fun interface MediaSenderRoomFactory {
|
||||
/**
|
||||
* Create a [MediaSender] for the given [JoinedRoom], with timeline mode Live.
|
||||
*/
|
||||
fun create(
|
||||
room: JoinedRoom,
|
||||
): MediaSender
|
||||
}
|
||||
|
||||
interface MediaSender {
|
||||
suspend fun preProcessMedia(
|
||||
uri: Uri,
|
||||
mimeType: String,
|
||||
mediaOptimizationConfig: MediaOptimizationConfig,
|
||||
): Result<MediaUploadInfo> {
|
||||
Timber.d("Pre-processing media | uri: ${mediaId(uri)} | mimeType: $mimeType")
|
||||
return preProcessor
|
||||
.process(
|
||||
uri = uri,
|
||||
mimeType = mimeType,
|
||||
deleteOriginal = false,
|
||||
mediaOptimizationConfig = mediaOptimizationConfig,
|
||||
)
|
||||
}
|
||||
): Result<MediaUploadInfo>
|
||||
|
||||
suspend fun sendPreProcessedMedia(
|
||||
mediaUploadInfo: MediaUploadInfo,
|
||||
caption: String?,
|
||||
formattedCaption: String?,
|
||||
inReplyToEventId: EventId?,
|
||||
): Result<Unit> {
|
||||
val mediaLogId = mediaId(mediaUploadInfo.file)
|
||||
return getTimeline().flatMap {
|
||||
Timber.d("Started sending media $mediaLogId using timeline: ${it.mode}")
|
||||
it.sendMedia(
|
||||
uploadInfo = mediaUploadInfo,
|
||||
caption = caption,
|
||||
formattedCaption = formattedCaption,
|
||||
inReplyToEventId = inReplyToEventId,
|
||||
)
|
||||
}
|
||||
.handleSendResult(mediaLogId)
|
||||
}
|
||||
): Result<Unit>
|
||||
|
||||
suspend fun sendMedia(
|
||||
uri: Uri,
|
||||
@@ -84,147 +52,14 @@ class MediaSender(
|
||||
formattedCaption: String? = null,
|
||||
inReplyToEventId: EventId? = null,
|
||||
mediaOptimizationConfig: MediaOptimizationConfig,
|
||||
): Result<Unit> {
|
||||
return preProcessor
|
||||
.process(
|
||||
uri = uri,
|
||||
mimeType = mimeType,
|
||||
deleteOriginal = false,
|
||||
mediaOptimizationConfig = mediaOptimizationConfig,
|
||||
)
|
||||
.flatMapCatching { info ->
|
||||
getTimeline().getOrThrow().sendMedia(
|
||||
uploadInfo = info,
|
||||
caption = caption,
|
||||
formattedCaption = formattedCaption,
|
||||
inReplyToEventId = inReplyToEventId,
|
||||
)
|
||||
}
|
||||
.handleSendResult(mediaId(uri))
|
||||
}
|
||||
): Result<Unit>
|
||||
|
||||
suspend fun sendVoiceMessage(
|
||||
uri: Uri,
|
||||
mimeType: String,
|
||||
waveForm: List<Float>,
|
||||
inReplyToEventId: EventId? = null,
|
||||
): Result<Unit> {
|
||||
return preProcessor
|
||||
.process(
|
||||
uri = uri,
|
||||
mimeType = mimeType,
|
||||
deleteOriginal = true,
|
||||
mediaOptimizationConfig = mediaOptimizationConfigProvider.get(),
|
||||
)
|
||||
.flatMapCatching { info ->
|
||||
val audioInfo = (info as MediaUploadInfo.Audio).audioInfo
|
||||
val newInfo = MediaUploadInfo.VoiceMessage(
|
||||
file = info.file,
|
||||
audioInfo = audioInfo,
|
||||
waveform = waveForm,
|
||||
)
|
||||
getTimeline().getOrThrow().sendMedia(
|
||||
uploadInfo = newInfo,
|
||||
caption = null,
|
||||
formattedCaption = null,
|
||||
inReplyToEventId = inReplyToEventId,
|
||||
)
|
||||
}
|
||||
.handleSendResult(mediaId(uri))
|
||||
}
|
||||
): Result<Unit>
|
||||
|
||||
private fun Result<Unit>.handleSendResult(mediaId: String) = this
|
||||
.onFailure { error ->
|
||||
val job = ongoingUploadJobs.remove(Job)
|
||||
Timber.e(error, "Sending media $mediaId failed. Removing ongoing upload job. Total: ${ongoingUploadJobs.size}")
|
||||
if (error !is CancellationException) {
|
||||
job?.cancel()
|
||||
}
|
||||
}
|
||||
.onSuccess {
|
||||
Timber.d("Sent media $mediaId successfully. Removing ongoing upload job. Total: ${ongoingUploadJobs.size}")
|
||||
ongoingUploadJobs.remove(Job)
|
||||
}
|
||||
|
||||
private suspend fun Timeline.sendMedia(
|
||||
uploadInfo: MediaUploadInfo,
|
||||
caption: String?,
|
||||
formattedCaption: String?,
|
||||
inReplyToEventId: EventId?,
|
||||
): Result<Unit> {
|
||||
val handler = when (uploadInfo) {
|
||||
is MediaUploadInfo.Image -> {
|
||||
sendImage(
|
||||
file = uploadInfo.file,
|
||||
thumbnailFile = uploadInfo.thumbnailFile,
|
||||
imageInfo = uploadInfo.imageInfo,
|
||||
caption = caption,
|
||||
formattedCaption = formattedCaption,
|
||||
inReplyToEventId = inReplyToEventId,
|
||||
)
|
||||
}
|
||||
is MediaUploadInfo.Video -> {
|
||||
sendVideo(
|
||||
file = uploadInfo.file,
|
||||
thumbnailFile = uploadInfo.thumbnailFile,
|
||||
videoInfo = uploadInfo.videoInfo,
|
||||
caption = caption,
|
||||
formattedCaption = formattedCaption,
|
||||
inReplyToEventId = inReplyToEventId,
|
||||
)
|
||||
}
|
||||
is MediaUploadInfo.Audio -> {
|
||||
sendAudio(
|
||||
file = uploadInfo.file,
|
||||
audioInfo = uploadInfo.audioInfo,
|
||||
caption = caption,
|
||||
formattedCaption = formattedCaption,
|
||||
inReplyToEventId = inReplyToEventId,
|
||||
)
|
||||
}
|
||||
is MediaUploadInfo.VoiceMessage -> {
|
||||
sendVoiceMessage(
|
||||
file = uploadInfo.file,
|
||||
audioInfo = uploadInfo.audioInfo,
|
||||
waveform = uploadInfo.waveform,
|
||||
inReplyToEventId = inReplyToEventId,
|
||||
)
|
||||
}
|
||||
is MediaUploadInfo.AnyFile -> {
|
||||
sendFile(
|
||||
file = uploadInfo.file,
|
||||
fileInfo = uploadInfo.fileInfo,
|
||||
caption = caption,
|
||||
formattedCaption = formattedCaption,
|
||||
inReplyToEventId = inReplyToEventId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// We handle the cancellations here manually, so we suppress the warning
|
||||
@Suppress("RunCatchingNotAllowed")
|
||||
return handler
|
||||
.mapCatching { uploadHandler ->
|
||||
Timber.d("Added ongoing upload job, total: ${ongoingUploadJobs.size + 1}")
|
||||
ongoingUploadJobs[Job] = uploadHandler
|
||||
uploadHandler.await()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getTimeline(): Result<Timeline> {
|
||||
return when (timelineMode) {
|
||||
is Timeline.Mode.Thread -> {
|
||||
room.createTimeline(CreateTimelineParams.Threaded(threadRootEventId = timelineMode.threadRootId))
|
||||
}
|
||||
else -> Result.success(room.liveTimeline)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up any temporary files or resources used during the media processing.
|
||||
*/
|
||||
fun cleanUp() = preProcessor.cleanUp()
|
||||
fun cleanUp()
|
||||
}
|
||||
|
||||
private fun mediaId(uri: Uri?): String = uri?.path.orEmpty().hash()
|
||||
private fun mediaId(file: File): String = file.path.orEmpty().hash()
|
||||
|
||||
@@ -42,4 +42,7 @@ dependencies {
|
||||
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(projects.services.toolbox.test)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.preferences.test)
|
||||
testImplementation(projects.libraries.mediaupload.test)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2025 New Vector 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.libraries.mediaupload.impl
|
||||
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.mediaupload.api.MaxUploadSizeProvider
|
||||
|
||||
/**
|
||||
* Provides the maximum upload size allowed by the Matrix server.
|
||||
*/
|
||||
@ContributesBinding(SessionScope::class)
|
||||
class DefaultMaxUploadSizeProvider(
|
||||
private val matrixClient: MatrixClient,
|
||||
) : MaxUploadSizeProvider {
|
||||
override suspend fun getMaxUploadSize(): Result<Long> {
|
||||
return matrixClient.getMaxFileUploadSize()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector 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.libraries.mediaupload.impl
|
||||
|
||||
import android.net.Uri
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.androidutils.hash.hash
|
||||
import io.element.android.libraries.core.extensions.flatMap
|
||||
import io.element.android.libraries.core.extensions.flatMapCatching
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
|
||||
import io.element.android.libraries.matrix.api.room.CreateTimelineParams
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfig
|
||||
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider
|
||||
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
||||
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||
import io.element.android.libraries.mediaupload.api.MediaSenderFactory
|
||||
import io.element.android.libraries.mediaupload.api.MediaSenderRoomFactory
|
||||
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Job
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
@ContributesBinding(RoomScope::class)
|
||||
class DefaultMediaSenderFactory(
|
||||
private val preProcessor: MediaPreProcessor,
|
||||
private val room: JoinedRoom,
|
||||
private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider,
|
||||
) : MediaSenderFactory {
|
||||
override fun create(
|
||||
timelineMode: Timeline.Mode,
|
||||
): MediaSender {
|
||||
return DefaultMediaSender(
|
||||
preProcessor = preProcessor,
|
||||
room = room,
|
||||
timelineMode = timelineMode,
|
||||
mediaOptimizationConfigProvider = mediaOptimizationConfigProvider,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
class DefaultMediaSenderRoomFactory(
|
||||
private val preProcessor: MediaPreProcessor,
|
||||
private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider,
|
||||
) : MediaSenderRoomFactory {
|
||||
override fun create(
|
||||
room: JoinedRoom,
|
||||
): MediaSender {
|
||||
return DefaultMediaSender(
|
||||
preProcessor = preProcessor,
|
||||
room = room,
|
||||
timelineMode = Timeline.Mode.Live,
|
||||
mediaOptimizationConfigProvider = mediaOptimizationConfigProvider,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class DefaultMediaSender(
|
||||
private val preProcessor: MediaPreProcessor,
|
||||
private val room: JoinedRoom,
|
||||
private val timelineMode: Timeline.Mode,
|
||||
private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider,
|
||||
) : MediaSender {
|
||||
private val ongoingUploadJobs = ConcurrentHashMap<Job.Key, MediaUploadHandler>()
|
||||
val hasOngoingMediaUploads get() = ongoingUploadJobs.isNotEmpty()
|
||||
|
||||
override suspend fun preProcessMedia(
|
||||
uri: Uri,
|
||||
mimeType: String,
|
||||
mediaOptimizationConfig: MediaOptimizationConfig,
|
||||
): Result<MediaUploadInfo> {
|
||||
Timber.d("Pre-processing media | uri: ${mediaId(uri)} | mimeType: $mimeType")
|
||||
return preProcessor
|
||||
.process(
|
||||
uri = uri,
|
||||
mimeType = mimeType,
|
||||
deleteOriginal = false,
|
||||
mediaOptimizationConfig = mediaOptimizationConfig,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun sendPreProcessedMedia(
|
||||
mediaUploadInfo: MediaUploadInfo,
|
||||
caption: String?,
|
||||
formattedCaption: String?,
|
||||
inReplyToEventId: EventId?,
|
||||
): Result<Unit> {
|
||||
val mediaLogId = mediaId(mediaUploadInfo.file)
|
||||
return getTimeline().flatMap {
|
||||
Timber.d("Started sending media $mediaLogId using timeline: ${it.mode}")
|
||||
it.sendMedia(
|
||||
uploadInfo = mediaUploadInfo,
|
||||
caption = caption,
|
||||
formattedCaption = formattedCaption,
|
||||
inReplyToEventId = inReplyToEventId,
|
||||
)
|
||||
}
|
||||
.handleSendResult(mediaLogId)
|
||||
}
|
||||
|
||||
override suspend fun sendMedia(
|
||||
uri: Uri,
|
||||
mimeType: String,
|
||||
caption: String?,
|
||||
formattedCaption: String?,
|
||||
inReplyToEventId: EventId?,
|
||||
mediaOptimizationConfig: MediaOptimizationConfig,
|
||||
): Result<Unit> {
|
||||
return preProcessor
|
||||
.process(
|
||||
uri = uri,
|
||||
mimeType = mimeType,
|
||||
deleteOriginal = false,
|
||||
mediaOptimizationConfig = mediaOptimizationConfig,
|
||||
)
|
||||
.flatMapCatching { info ->
|
||||
getTimeline().getOrThrow().sendMedia(
|
||||
uploadInfo = info,
|
||||
caption = caption,
|
||||
formattedCaption = formattedCaption,
|
||||
inReplyToEventId = inReplyToEventId,
|
||||
)
|
||||
}
|
||||
.handleSendResult(mediaId(uri))
|
||||
}
|
||||
|
||||
override suspend fun sendVoiceMessage(
|
||||
uri: Uri,
|
||||
mimeType: String,
|
||||
waveForm: List<Float>,
|
||||
inReplyToEventId: EventId?,
|
||||
): Result<Unit> {
|
||||
return preProcessor
|
||||
.process(
|
||||
uri = uri,
|
||||
mimeType = mimeType,
|
||||
deleteOriginal = true,
|
||||
mediaOptimizationConfig = mediaOptimizationConfigProvider.get(),
|
||||
)
|
||||
.flatMapCatching { info ->
|
||||
val audioInfo = (info as MediaUploadInfo.Audio).audioInfo
|
||||
val newInfo = MediaUploadInfo.VoiceMessage(
|
||||
file = info.file,
|
||||
audioInfo = audioInfo,
|
||||
waveform = waveForm,
|
||||
)
|
||||
getTimeline().getOrThrow().sendMedia(
|
||||
uploadInfo = newInfo,
|
||||
caption = null,
|
||||
formattedCaption = null,
|
||||
inReplyToEventId = inReplyToEventId,
|
||||
)
|
||||
}
|
||||
.handleSendResult(mediaId(uri))
|
||||
}
|
||||
|
||||
private fun Result<Unit>.handleSendResult(mediaId: String) = this
|
||||
.onFailure { error ->
|
||||
val job = ongoingUploadJobs.remove(Job)
|
||||
Timber.e(error, "Sending media $mediaId failed. Removing ongoing upload job. Total: ${ongoingUploadJobs.size}")
|
||||
if (error !is CancellationException) {
|
||||
job?.cancel()
|
||||
}
|
||||
}
|
||||
.onSuccess {
|
||||
Timber.d("Sent media $mediaId successfully. Removing ongoing upload job. Total: ${ongoingUploadJobs.size}")
|
||||
ongoingUploadJobs.remove(Job)
|
||||
}
|
||||
|
||||
private suspend fun Timeline.sendMedia(
|
||||
uploadInfo: MediaUploadInfo,
|
||||
caption: String?,
|
||||
formattedCaption: String?,
|
||||
inReplyToEventId: EventId?,
|
||||
): Result<Unit> {
|
||||
val handler = when (uploadInfo) {
|
||||
is MediaUploadInfo.Image -> {
|
||||
sendImage(
|
||||
file = uploadInfo.file,
|
||||
thumbnailFile = uploadInfo.thumbnailFile,
|
||||
imageInfo = uploadInfo.imageInfo,
|
||||
caption = caption,
|
||||
formattedCaption = formattedCaption,
|
||||
inReplyToEventId = inReplyToEventId,
|
||||
)
|
||||
}
|
||||
is MediaUploadInfo.Video -> {
|
||||
sendVideo(
|
||||
file = uploadInfo.file,
|
||||
thumbnailFile = uploadInfo.thumbnailFile,
|
||||
videoInfo = uploadInfo.videoInfo,
|
||||
caption = caption,
|
||||
formattedCaption = formattedCaption,
|
||||
inReplyToEventId = inReplyToEventId,
|
||||
)
|
||||
}
|
||||
is MediaUploadInfo.Audio -> {
|
||||
sendAudio(
|
||||
file = uploadInfo.file,
|
||||
audioInfo = uploadInfo.audioInfo,
|
||||
caption = caption,
|
||||
formattedCaption = formattedCaption,
|
||||
inReplyToEventId = inReplyToEventId,
|
||||
)
|
||||
}
|
||||
is MediaUploadInfo.VoiceMessage -> {
|
||||
sendVoiceMessage(
|
||||
file = uploadInfo.file,
|
||||
audioInfo = uploadInfo.audioInfo,
|
||||
waveform = uploadInfo.waveform,
|
||||
inReplyToEventId = inReplyToEventId,
|
||||
)
|
||||
}
|
||||
is MediaUploadInfo.AnyFile -> {
|
||||
sendFile(
|
||||
file = uploadInfo.file,
|
||||
fileInfo = uploadInfo.fileInfo,
|
||||
caption = caption,
|
||||
formattedCaption = formattedCaption,
|
||||
inReplyToEventId = inReplyToEventId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// We handle the cancellations here manually, so we suppress the warning
|
||||
@Suppress("RunCatchingNotAllowed")
|
||||
return handler
|
||||
.mapCatching { uploadHandler ->
|
||||
Timber.d("Added ongoing upload job, total: ${ongoingUploadJobs.size + 1}")
|
||||
ongoingUploadJobs[Job] = uploadHandler
|
||||
uploadHandler.await()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getTimeline(): Result<Timeline> {
|
||||
return when (timelineMode) {
|
||||
is Timeline.Mode.Thread -> {
|
||||
room.createTimeline(CreateTimelineParams.Threaded(threadRootEventId = timelineMode.threadRootId))
|
||||
}
|
||||
else -> Result.success(room.liveTimeline)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up any temporary files or resources used during the media processing.
|
||||
*/
|
||||
override fun cleanUp() = preProcessor.cleanUp()
|
||||
}
|
||||
|
||||
private fun mediaId(uri: Uri?): String = uri?.path.orEmpty().hash()
|
||||
private fun mediaId(file: File): String = file.path.orEmpty().hash()
|
||||
@@ -6,7 +6,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.mediaupload.api
|
||||
package io.element.android.libraries.mediaupload.impl
|
||||
|
||||
import android.net.Uri
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
@@ -19,6 +19,9 @@ import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
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
|
||||
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfig
|
||||
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider
|
||||
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
||||
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
||||
import io.element.android.libraries.preferences.api.store.VideoCompressionPreset
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
@@ -33,7 +36,7 @@ import org.robolectric.RobolectricTestRunner
|
||||
import java.io.File
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class MediaSenderTest {
|
||||
class DefaultMediaSenderTest {
|
||||
private val mediaOptimizationConfig = MediaOptimizationConfig(
|
||||
compressImages = true,
|
||||
videoCompressionPreset = VideoCompressionPreset.STANDARD,
|
||||
@@ -42,7 +45,7 @@ class MediaSenderTest {
|
||||
@Test
|
||||
fun `given an attachment when sending it the preprocessor always runs`() = runTest {
|
||||
val preProcessor = FakeMediaPreProcessor()
|
||||
val sender = createMediaSender(
|
||||
val sender = createDefaultMediaSender(
|
||||
preProcessor = preProcessor,
|
||||
room = FakeJoinedRoom(
|
||||
liveTimeline = FakeTimeline().apply {
|
||||
@@ -77,7 +80,7 @@ class MediaSenderTest {
|
||||
sendImageLambda = sendImageResult
|
||||
},
|
||||
)
|
||||
val sender = createMediaSender(room = room)
|
||||
val sender = createDefaultMediaSender(room = room)
|
||||
|
||||
val uri = Uri.parse("content://image.jpg")
|
||||
sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, mediaOptimizationConfig = mediaOptimizationConfig)
|
||||
@@ -88,7 +91,7 @@ class MediaSenderTest {
|
||||
val preProcessor = FakeMediaPreProcessor().apply {
|
||||
givenResult(Result.failure(Exception()))
|
||||
}
|
||||
val sender = createMediaSender(preProcessor)
|
||||
val sender = createDefaultMediaSender(preProcessor)
|
||||
|
||||
val uri = Uri.parse("content://image.jpg")
|
||||
val result = sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, mediaOptimizationConfig = mediaOptimizationConfig)
|
||||
@@ -110,7 +113,7 @@ class MediaSenderTest {
|
||||
sendImageLambda = sendImageResult
|
||||
},
|
||||
)
|
||||
val sender = createMediaSender(
|
||||
val sender = createDefaultMediaSender(
|
||||
preProcessor = preProcessor,
|
||||
room = room,
|
||||
)
|
||||
@@ -133,7 +136,7 @@ class MediaSenderTest {
|
||||
sendFileLambda = sendFileResult
|
||||
},
|
||||
)
|
||||
val sender = createMediaSender(room = room)
|
||||
val sender = createDefaultMediaSender(room = room)
|
||||
val sendJob = launch {
|
||||
val uri = Uri.parse("content://image.jpg")
|
||||
sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, mediaOptimizationConfig = mediaOptimizationConfig)
|
||||
@@ -155,11 +158,11 @@ class MediaSenderTest {
|
||||
sendFileResult.assertions().isCalledOnce()
|
||||
}
|
||||
|
||||
private fun createMediaSender(
|
||||
private fun createDefaultMediaSender(
|
||||
preProcessor: MediaPreProcessor = FakeMediaPreProcessor(),
|
||||
room: JoinedRoom = FakeJoinedRoom(),
|
||||
mediaOptimizationConfigProvider: MediaOptimizationConfigProvider = MediaOptimizationConfigProvider { mediaOptimizationConfig },
|
||||
) = MediaSender(
|
||||
) = DefaultMediaSender(
|
||||
preProcessor = preProcessor,
|
||||
room = room,
|
||||
timelineMode = Timeline.Mode.Live,
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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.libraries.mediaupload.test
|
||||
|
||||
import android.net.Uri
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfig
|
||||
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeMediaSender(
|
||||
private val preProcessMediaResult: () -> Result<MediaUploadInfo> = { lambdaError() },
|
||||
private val sendPreProcessedMediaResult: () -> Result<Unit> = { lambdaError() },
|
||||
private val sendMediaResult: () -> Result<Unit> = { lambdaError() },
|
||||
private val sendVoiceMessageResult: () -> Result<Unit> = { lambdaError() },
|
||||
private val cleanUpResult: () -> Unit = { lambdaError() },
|
||||
) : MediaSender {
|
||||
override suspend fun preProcessMedia(
|
||||
uri: Uri,
|
||||
mimeType: String,
|
||||
mediaOptimizationConfig: MediaOptimizationConfig,
|
||||
): Result<MediaUploadInfo> {
|
||||
return preProcessMediaResult()
|
||||
}
|
||||
|
||||
override suspend fun sendPreProcessedMedia(
|
||||
mediaUploadInfo: MediaUploadInfo,
|
||||
caption: String?,
|
||||
formattedCaption: String?,
|
||||
inReplyToEventId: EventId?,
|
||||
): Result<Unit> {
|
||||
return sendPreProcessedMediaResult()
|
||||
}
|
||||
|
||||
override suspend fun sendMedia(
|
||||
uri: Uri,
|
||||
mimeType: String,
|
||||
caption: String?,
|
||||
formattedCaption: String?,
|
||||
inReplyToEventId: EventId?,
|
||||
mediaOptimizationConfig: MediaOptimizationConfig,
|
||||
): Result<Unit> {
|
||||
return sendMediaResult()
|
||||
}
|
||||
|
||||
override suspend fun sendVoiceMessage(
|
||||
uri: Uri,
|
||||
mimeType: String,
|
||||
waveForm: List<Float>,
|
||||
inReplyToEventId: EventId?,
|
||||
): Result<Unit> {
|
||||
return sendVoiceMessageResult()
|
||||
}
|
||||
|
||||
override fun cleanUp() {
|
||||
cleanUpResult()
|
||||
}
|
||||
}
|
||||
@@ -84,6 +84,7 @@ dependencies {
|
||||
testImplementation(projects.features.enterprise.test)
|
||||
testImplementation(projects.features.lockscreen.test)
|
||||
testImplementation(projects.features.networkmonitor.test)
|
||||
testImplementation(projects.services.appnavstate.impl)
|
||||
testImplementation(projects.services.appnavstate.test)
|
||||
testImplementation(projects.services.toolbox.impl)
|
||||
testImplementation(projects.services.toolbox.test)
|
||||
|
||||
@@ -40,6 +40,7 @@ import io.element.android.libraries.push.impl.push.FakeOnNotifiableEventReceived
|
||||
import io.element.android.libraries.push.impl.push.OnNotifiableEventReceived
|
||||
import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner
|
||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||
import io.element.android.services.appnavstate.impl.DefaultActiveRoomsHolder
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
import io.element.android.services.toolbox.test.strings.FakeStringProvider
|
||||
@@ -482,7 +483,7 @@ class NotificationBroadcastReceiverHandlerTest {
|
||||
onNotifiableEventReceived: OnNotifiableEventReceived = FakeOnNotifiableEventReceived(),
|
||||
stringProvider: StringProvider = FakeStringProvider(),
|
||||
replyMessageExtractor: ReplyMessageExtractor = FakeReplyMessageExtractor(),
|
||||
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
|
||||
activeRoomsHolder: ActiveRoomsHolder = DefaultActiveRoomsHolder(),
|
||||
): NotificationBroadcastReceiverHandler {
|
||||
return NotificationBroadcastReceiverHandler(
|
||||
appCoroutineScope = this,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import extension.setupDependencyInjection
|
||||
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
@@ -16,8 +14,6 @@ android {
|
||||
namespace = "io.element.android.libraries.recentemojis.api"
|
||||
}
|
||||
|
||||
setupDependencyInjection()
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
|
||||
@@ -8,17 +8,6 @@
|
||||
|
||||
package io.element.android.libraries.recentemojis.api
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Inject
|
||||
class AddRecentEmoji(
|
||||
private val client: MatrixClient,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) {
|
||||
suspend operator fun invoke(emoji: String): Result<Unit> = withContext(dispatchers.io) {
|
||||
client.addRecentEmoji(emoji)
|
||||
}
|
||||
fun interface AddRecentEmoji {
|
||||
suspend operator fun invoke(emoji: String): Result<Unit>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2025 New Vector 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.libraries.recentemojis.impl
|
||||
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.recentemojis.api.AddRecentEmoji
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
class DefaultAddRecentEmoji(
|
||||
private val client: MatrixClient,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) : AddRecentEmoji {
|
||||
override suspend operator fun invoke(emoji: String): Result<Unit> = withContext(dispatchers.io) {
|
||||
client.addRecentEmoji(emoji)
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,8 @@ import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.api.mxc.MxcTools
|
||||
import io.element.android.libraries.matrix.test.media.FakeMatrixMediaLoader
|
||||
import io.element.android.libraries.matrix.test.mxc.FakeMxcTools
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
@@ -131,7 +131,7 @@ private fun createDefaultVoiceMessageMediaRepo(
|
||||
mxcUri: String = MXC_URI,
|
||||
) = DefaultVoiceMessageMediaRepo(
|
||||
cacheDir = temporaryFolder.root,
|
||||
mxcTools = MxcTools(),
|
||||
mxcTools = FakeMxcTools(),
|
||||
matrixMediaLoader = matrixMediaLoader,
|
||||
mediaSource = MediaSource(
|
||||
url = mxcUri,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import extension.setupDependencyInjection
|
||||
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
@@ -15,8 +13,6 @@ android {
|
||||
namespace = "io.element.android.libraries.workmanager.api"
|
||||
}
|
||||
|
||||
setupDependencyInjection()
|
||||
|
||||
dependencies {
|
||||
api(libs.androidx.workmanager.runtime)
|
||||
|
||||
|
||||
@@ -22,6 +22,10 @@ import org.gradle.plugin.use.PluginDependency
|
||||
fun Project.setupDependencyInjection(
|
||||
generateNodeFactories: Boolean = shouldApplyAppyxCodegen(),
|
||||
) {
|
||||
if (project.path.endsWith(":api")) {
|
||||
error("api module should not use setupDependencyInjection(). Move the implementation to `:impl` module")
|
||||
}
|
||||
|
||||
val libs = the<LibrariesForLibs>()
|
||||
|
||||
// Apply Metro plugin and configure it
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import extension.setupDependencyInjection
|
||||
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2022-2024 New Vector Ltd.
|
||||
@@ -16,8 +14,6 @@ android {
|
||||
namespace = "io.element.android.services.appnavstate.api"
|
||||
}
|
||||
|
||||
setupDependencyInjection()
|
||||
|
||||
dependencies {
|
||||
implementation(libs.coroutines.core)
|
||||
implementation(libs.androidx.lifecycle.runtime)
|
||||
|
||||
@@ -8,63 +8,36 @@
|
||||
|
||||
package io.element.android.services.appnavstate.api
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* Holds the active rooms for a given session so they can be reused instead of instantiating new ones.
|
||||
*/
|
||||
@SingleIn(AppScope::class)
|
||||
@Inject
|
||||
class ActiveRoomsHolder {
|
||||
private val rooms = ConcurrentHashMap<SessionId, MutableSet<JoinedRoom>>()
|
||||
|
||||
interface ActiveRoomsHolder {
|
||||
/**
|
||||
* Adds a new held room for the given sessionId.
|
||||
*/
|
||||
fun addRoom(room: JoinedRoom) {
|
||||
val roomsForSessionId = rooms.getOrPut(key = room.sessionId, defaultValue = { mutableSetOf() })
|
||||
if (roomsForSessionId.none { it.roomId == room.roomId }) {
|
||||
// We don't want to add the same room multiple times
|
||||
roomsForSessionId.add(room)
|
||||
}
|
||||
}
|
||||
fun addRoom(room: JoinedRoom)
|
||||
|
||||
/**
|
||||
* Returns the last room added for the given [sessionId] or null if no room was added.
|
||||
*/
|
||||
fun getActiveRoom(sessionId: SessionId): JoinedRoom? {
|
||||
return rooms[sessionId]?.lastOrNull()
|
||||
}
|
||||
fun getActiveRoom(sessionId: SessionId): JoinedRoom?
|
||||
|
||||
/**
|
||||
* Returns an active room associated to the given [sessionId], with the given [roomId], or null if none match.
|
||||
*/
|
||||
fun getActiveRoomMatching(sessionId: SessionId, roomId: RoomId): JoinedRoom? {
|
||||
return rooms[sessionId]?.find { it.roomId == roomId }
|
||||
}
|
||||
fun getActiveRoomMatching(sessionId: SessionId, roomId: RoomId): JoinedRoom?
|
||||
|
||||
/**
|
||||
* Removes any room matching the provided [sessionId] and [roomId].
|
||||
*/
|
||||
fun removeRoom(sessionId: SessionId, roomId: RoomId) {
|
||||
val roomsForSessionId = rooms[sessionId] ?: return
|
||||
roomsForSessionId.removeIf { it.roomId == roomId }
|
||||
}
|
||||
fun removeRoom(sessionId: SessionId, roomId: RoomId)
|
||||
|
||||
/**
|
||||
* Clears all the rooms for the given sessionId.
|
||||
*/
|
||||
fun clear(sessionId: SessionId) {
|
||||
val activeRooms = rooms.remove(sessionId) ?: return
|
||||
for (room in activeRooms) {
|
||||
// Destroy the room to reset the live timelines
|
||||
room.destroy()
|
||||
}
|
||||
}
|
||||
fun clear(sessionId: SessionId)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2025 New Vector 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.services.appnavstate.impl
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
@SingleIn(AppScope::class)
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultActiveRoomsHolder : ActiveRoomsHolder {
|
||||
private val rooms = ConcurrentHashMap<SessionId, MutableSet<JoinedRoom>>()
|
||||
|
||||
override fun addRoom(room: JoinedRoom) {
|
||||
val roomsForSessionId = rooms.getOrPut(key = room.sessionId, defaultValue = { mutableSetOf() })
|
||||
if (roomsForSessionId.none { it.roomId == room.roomId }) {
|
||||
// We don't want to add the same room multiple times
|
||||
roomsForSessionId.add(room)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getActiveRoom(sessionId: SessionId): JoinedRoom? {
|
||||
return rooms[sessionId]?.lastOrNull()
|
||||
}
|
||||
|
||||
override fun getActiveRoomMatching(sessionId: SessionId, roomId: RoomId): JoinedRoom? {
|
||||
return rooms[sessionId]?.find { it.roomId == roomId }
|
||||
}
|
||||
|
||||
override fun removeRoom(sessionId: SessionId, roomId: RoomId) {
|
||||
val roomsForSessionId = rooms[sessionId] ?: return
|
||||
roomsForSessionId.removeIf { it.roomId == roomId }
|
||||
}
|
||||
|
||||
override fun clear(sessionId: SessionId) {
|
||||
val activeRooms = rooms.remove(sessionId) ?: return
|
||||
for (room in activeRooms) {
|
||||
// Destroy the room to reset the live timelines
|
||||
room.destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user