Send media: first working implementation. Then, need to fix local echo and handle other types than image.

This commit is contained in:
ganfra 2019-04-04 19:55:58 +02:00
parent 18591d0287
commit c47eeb9cec
17 changed files with 293 additions and 65 deletions

View File

@ -0,0 +1,32 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package im.vector.matrix.android.api.session.content

import android.os.Parcelable
import kotlinx.android.parcel.Parcelize

@Parcelize
data class ContentAttachmentData(
val size: Long = 0,
val duration: Long = 0,
val date: Long = 0,
val height: Long = 0,
val width: Long = 0,
val name: String? = null,
val path: String? = null,
val mimeType: String? = null
) : Parcelable

View File

@ -19,7 +19,7 @@ package im.vector.matrix.android.api.session.room.send
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.session.room.media.MediaAttachment
import im.vector.matrix.android.api.session.content.ContentAttachmentData

/**
* This interface defines methods to send events in a room. It's implemented at the room level.
@ -34,6 +34,6 @@ interface SendService {
*/
fun sendTextMessage(text: String, callback: MatrixCallback<Event>): Cancelable

fun sendMedia(attachment: MediaAttachment, callback: MatrixCallback<Event>): Cancelable
fun sendMedia(attachment: ContentAttachmentData, callback: MatrixCallback<Event>): Cancelable

}

View File

@ -0,0 +1,25 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package im.vector.matrix.android.internal.session.content

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)
data class ContentUploadResponse(
@Json(name = "content_uri") val contentUri: String
)

View File

@ -0,0 +1,76 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package im.vector.matrix.android.internal.session.content

import arrow.core.Try
import arrow.core.Try.Companion.raise
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.matrix.android.internal.di.MoshiProvider
import okhttp3.HttpUrl
import okhttp3.MediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import java.io.File
import java.io.IOException


internal class ContentUploader(private val okHttpClient: OkHttpClient,
private val sessionParams: SessionParams) {

private val moshi = MoshiProvider.providesMoshi()
private val responseAdapter = moshi.adapter(ContentUploadResponse::class.java)

fun uploadFile(attachment: ContentAttachmentData): Try<ContentUploadResponse> {
if (attachment.path == null || attachment.mimeType == null) {
return raise(RuntimeException())
}
val file = File(attachment.path)
val urlString = sessionParams.homeServerConnectionConfig.homeServerUri.toString() + URI_PREFIX_CONTENT_API + "upload"

val urlBuilder = HttpUrl.parse(urlString)?.newBuilder()
?: return raise(RuntimeException())

val httpUrl = urlBuilder
.addQueryParameter(
"filename", attachment.name
).build()

val requestBody = RequestBody.create(
MediaType.parse(attachment.mimeType),
file
)
val request = Request.Builder()
.url(httpUrl)
.post(requestBody)
.build()

return Try {
okHttpClient.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
throw IOException()
} else {
response.body()?.source()?.let {
responseAdapter.fromJson(it)
}
?: throw IOException()
}
}
}
}
}

View File

@ -0,0 +1,80 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package im.vector.matrix.android.internal.session.content

import android.content.Context
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.toContent
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
import im.vector.matrix.android.internal.di.MatrixKoinComponent
import im.vector.matrix.android.internal.session.room.send.SendEventWorker
import im.vector.matrix.android.internal.util.WorkerParamsFactory
import org.koin.standalone.inject

internal class UploadContentWorker(context: Context, params: WorkerParameters)
: Worker(context, params), MatrixKoinComponent {

private val mediaUploader by inject<ContentUploader>()

@JsonClass(generateAdapter = true)
internal data class Params(
val roomId: String,
val event: Event,
val attachment: ContentAttachmentData
)

override fun doWork(): Result {
val params = WorkerParamsFactory.fromData<Params>(inputData)
?: return Result.failure()

return mediaUploader
.uploadFile(params.attachment)
.fold({ handleFailure() }, { handleSuccess(params, it) })
}

private fun handleFailure(): Result {
return Result.retry()
}

private fun handleSuccess(params: Params, contentUploadResponse: ContentUploadResponse): Result {
val event = updateEvent(params.event, contentUploadResponse.contentUri)
val sendParams = SendEventWorker.Params(params.roomId, event)
return Result.success(WorkerParamsFactory.toData(sendParams))
}

private fun updateEvent(event: Event, url: String): Event {
val messageContent: MessageContent = event.content.toModel() ?: return event
val updatedContent = when (messageContent) {
is MessageImageContent -> messageContent.update(url)
else -> messageContent
}
return event.copy(content = updatedContent.toContent())
}

private fun MessageImageContent.update(url: String): MessageImageContent {
return copy(url = url)
}


}

View File

@ -23,7 +23,6 @@ import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.GroupEntity
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.util.WorkerParamsFactory

View File

@ -23,7 +23,7 @@ import im.vector.matrix.android.internal.session.room.members.RoomMemberExtracto
import im.vector.matrix.android.internal.session.room.read.DefaultReadService
import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask
import im.vector.matrix.android.internal.session.room.send.DefaultSendService
import im.vector.matrix.android.internal.session.room.send.EventFactory
import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineService
import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventTask
import im.vector.matrix.android.internal.session.room.timeline.PaginationTask
@ -35,7 +35,7 @@ internal class RoomFactory(private val loadRoomMembersTask: LoadRoomMembersTask,
private val paginationTask: PaginationTask,
private val contextOfEventTask: GetContextOfEventTask,
private val setReadMarkersTask: SetReadMarkersTask,
private val eventFactory: EventFactory,
private val eventFactory: LocalEchoEventFactory,
private val taskExecutor: TaskExecutor) {

fun instantiate(roomId: String): Room {

View File

@ -17,12 +17,12 @@
package im.vector.matrix.android.internal.session.room

import im.vector.matrix.android.internal.session.DefaultSession
import im.vector.matrix.android.internal.session.room.media.MediaUploader
import im.vector.matrix.android.internal.session.content.ContentUploader
import im.vector.matrix.android.internal.session.room.members.DefaultLoadRoomMembersTask
import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTask
import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask
import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask
import im.vector.matrix.android.internal.session.room.send.EventFactory
import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory
import im.vector.matrix.android.internal.session.room.timeline.*
import org.koin.dsl.module.module
import retrofit2.Retrofit
@ -58,11 +58,11 @@ class RoomModule {
}

scope(DefaultSession.SCOPE) {
EventFactory(get())
LocalEchoEventFactory(get())
}

scope(DefaultSession.SCOPE) {
MediaUploader(get(), get())
ContentUploader(get(), get())
}

scope(DefaultSession.SCOPE) {

View File

@ -18,7 +18,6 @@

package im.vector.matrix.android.internal.session.room.media

import android.net.Uri
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize

@ -30,7 +29,6 @@ data class MediaAttachment(
val height: Long = 0,
val width: Long = 0,
val name: String? = null,
val thumbnail: Uri? = null,
val path: String? = null,
val mimeType: String? = null
) : Parcelable

View File

@ -20,11 +20,13 @@ package im.vector.matrix.android.internal.session.room.media

import arrow.core.Try
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.matrix.android.internal.session.content.URI_PREFIX_CONTENT_API
import okhttp3.HttpUrl
import okhttp3.MediaType
import okhttp3.MultipartBody
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import java.io.File
import java.io.IOException

@ -32,21 +34,28 @@ import java.io.IOException
internal class MediaUploader(private val okHttpClient: OkHttpClient,
private val sessionParams: SessionParams) {

fun uploadFile(attachment: MediaAttachment): Try<String> {
fun uploadFile(attachment: ContentAttachmentData): Try<String> {
if (attachment.path == null || attachment.mimeType == null) {
return Try.raise(RuntimeException())
}
val urlString = sessionParams.homeServerConnectionConfig.homeServerUri.toString() + URI_PREFIX_CONTENT_API + "upload"
val file = File(attachment.path)
val urlString = sessionParams.homeServerConnectionConfig.homeServerUri.toString() + URI_PREFIX_CONTENT_API + "upload"

// create RequestBody instance from file
val requestFile = RequestBody.create(
val urlBuilder = HttpUrl.parse(urlString)?.newBuilder()
?: return Try.raise(RuntimeException())

val httpUrl = urlBuilder
.addQueryParameter(
"filename", attachment.name
).build()

val requestBody = MultipartBody.create(
MediaType.parse(attachment.mimeType),
file
)
val request = Request.Builder()
.url(urlString)
.post(requestFile)
.url(httpUrl)
.post(requestBody)
.build()

return okHttpClient.newCall(request).execute().use { response ->

View File

@ -23,17 +23,19 @@ import androidx.work.Worker
import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.di.MatrixKoinComponent
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.matrix.android.internal.session.content.ContentUploader
import im.vector.matrix.android.internal.util.WorkerParamsFactory
import org.koin.standalone.inject

internal class UploadMediaWorker(context: Context, params: WorkerParameters)
: Worker(context, params), MatrixKoinComponent {

private val mediaUploader by inject<MediaUploader>()
private val mediaUploader by inject<ContentUploader>()

@JsonClass(generateAdapter = true)
internal data class Params(
val attachment: MediaAttachment
val attachment: ContentAttachmentData
)

override fun doWork(): Result {

View File

@ -35,7 +35,9 @@ internal class EventsPruner(monarchy: Monarchy) :
override val query = Monarchy.Query<EventEntity> { EventEntity.where(it, type = EventType.REDACTION) }

override fun processChanges(inserted: List<EventEntity>, updated: List<EventEntity>, deleted: List<EventEntity>) {
val redactionEvents = inserted.map { it.asDomain() }
val redactionEvents = inserted
.mapNotNull { it.asDomain().redacts }

val pruneEventWorkerParams = PruneEventWorker.Params(redactionEvents)
val workData = WorkerParamsFactory.toData(pruneEventWorkerParams)


View File

@ -21,7 +21,6 @@ import androidx.work.Worker
import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.internal.database.mapper.ContentMapper
import im.vector.matrix.android.internal.database.model.EventEntity
@ -37,8 +36,8 @@ internal class PruneEventWorker(context: Context,
) : Worker(context, workerParameters), MatrixKoinComponent {

@JsonClass(generateAdapter = true)
internal data class Params(
val redactionEvents: List<Event>
internal class Params(
val eventIdsToRedact: List<String>
)

private val monarchy by inject<Monarchy>()
@ -48,18 +47,19 @@ internal class PruneEventWorker(context: Context,
?: return Result.failure()

val result = monarchy.tryTransactionSync { realm ->
params.redactionEvents.forEach { event ->
pruneEvent(realm, event)
params.eventIdsToRedact.forEach { eventId ->
pruneEvent(realm, eventId)
}
}
return result.fold({ Result.retry() }, { Result.success() })
}

private fun pruneEvent(realm: Realm, redactionEvent: Event?) {
if (redactionEvent == null || redactionEvent.redacts.isNullOrEmpty()) {
private fun pruneEvent(realm: Realm, eventIdToRedact: String) {
if (eventIdToRedact.isEmpty()) {
return
}
val eventToPrune = EventEntity.where(realm, eventId = redactionEvent.redacts).findFirst()

val eventToPrune = EventEntity.where(realm, eventId = eventIdToRedact).findFirst()
?: return

val allowedKeys = computeAllowedKeys(eventToPrune.type)
@ -87,7 +87,7 @@ internal class PruneEventWorker(context: Context,
EventType.STATE_ROOM_ALIASES -> listOf("aliases")
EventType.STATE_CANONICAL_ALIAS -> listOf("alias")
EventType.FEEDBACK -> listOf("type", "target_event_id")
else -> emptyList()
else -> emptyList()
}
}


View File

@ -16,17 +16,23 @@

package im.vector.matrix.android.internal.session.room.send

import androidx.work.*
import androidx.work.BackoffPolicy
import androidx.work.Constraints
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequest
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.send.SendService
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.database.helper.add
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
import im.vector.matrix.android.internal.session.room.media.MediaAttachment
import im.vector.matrix.android.internal.session.room.media.UploadMediaWorker
import im.vector.matrix.android.internal.session.content.UploadContentWorker
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
import im.vector.matrix.android.internal.util.CancelableWork
import im.vector.matrix.android.internal.util.WorkerParamsFactory
@ -35,18 +41,19 @@ import java.util.concurrent.TimeUnit

private const val SEND_WORK = "SEND_WORK"
private const val BACKOFF_DELAY = 10_000L

private val WORK_CONSTRAINTS = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()

internal class DefaultSendService(private val roomId: String,
private val eventFactory: EventFactory,
private val eventFactory: LocalEchoEventFactory,
private val monarchy: Monarchy) : SendService {


override fun sendTextMessage(text: String, callback: MatrixCallback<Event>): Cancelable {
val event = eventFactory.createTextEvent(roomId, text)
saveLiveEvent(event)
saveLocalEcho(event)
val sendWork = createSendEventWork(event)
WorkManager.getInstance()
.beginUniqueWork(SEND_WORK, ExistingWorkPolicy.APPEND, sendWork)
@ -55,12 +62,12 @@ internal class DefaultSendService(private val roomId: String,
return CancelableWork(sendWork.id)
}

override fun sendMedia(attachment: MediaAttachment, callback: MatrixCallback<Event>): Cancelable {
override fun sendMedia(attachment: ContentAttachmentData, callback: MatrixCallback<Event>): Cancelable {
// Create an event with the media file path
val event = eventFactory.createImageEvent(roomId, attachment)
saveLiveEvent(event)

val uploadWork = createUploadMediaWork(attachment)
val event = eventFactory.createMediaEvent(roomId, attachment).also {
saveLocalEcho(it)
}
val uploadWork = createUploadMediaWork(event, attachment)
val sendWork = createSendEventWork(event)

WorkManager.getInstance()
@ -70,10 +77,10 @@ internal class DefaultSendService(private val roomId: String,
return CancelableWork(sendWork.id)
}

private fun saveLiveEvent(event: Event) {
private fun saveLocalEcho(event: Event) {
monarchy.tryTransactionAsync { realm ->
val chunkEntity = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)
?: return@tryTransactionAsync
?: return@tryTransactionAsync
chunkEntity.add(roomId, event, PaginationDirection.FORWARDS)
}
}
@ -89,11 +96,11 @@ internal class DefaultSendService(private val roomId: String,
.build()
}

private fun createUploadMediaWork(attachment: MediaAttachment): OneTimeWorkRequest {
val uploadMediaWorkerParams = UploadMediaWorker.Params(attachment)
private fun createUploadMediaWork(event: Event, attachment: ContentAttachmentData): OneTimeWorkRequest {
val uploadMediaWorkerParams = UploadContentWorker.Params(roomId, event, attachment)
val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams)

return OneTimeWorkRequestBuilder<UploadMediaWorker>()
return OneTimeWorkRequestBuilder<UploadContentWorker>()
.setConstraints(WORK_CONSTRAINTS)
.setInputData(uploadWorkData)
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)

View File

@ -17,38 +17,38 @@
package im.vector.matrix.android.internal.session.room.send

import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toContent
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.ImageInfo
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.internal.session.room.media.MediaAttachment

internal class EventFactory(private val credentials: Credentials) {
internal class LocalEchoEventFactory(private val credentials: Credentials) {

fun createTextEvent(roomId: String, text: String): Event {
val content = MessageTextContent(type = MessageType.MSGTYPE_TEXT, body = text)
return createEvent(roomId, content)
}

fun createImageEvent(roomId: String, attachment: MediaAttachment): Event {
fun createMediaEvent(roomId: String, attachment: ContentAttachmentData): Event {
val content = MessageImageContent(
type = MessageType.MSGTYPE_IMAGE,
body = attachment.name ?: "image",
info = ImageInfo(
mimeType = attachment.mimeType ?: "image/png",
width = attachment.width.toInt(),
height = attachment.height.toInt(),
size = attachment.size.toInt()
),
url = attachment.path
)
return createEvent(roomId, content)
}

fun updateImageEvent(event: Event, url: String): Event {
val imageContent = event.content.toModel<MessageImageContent>() ?: return event
val updatedContent = imageContent.copy(url = url)
return event.copy(content = updatedContent.toContent())
}

fun createEvent(roomId: String, content: Any? = null): Event {
private fun createEvent(roomId: String, content: Any? = null): Event {
return Event(
roomId = roomId,
originServerTs = dummyOriginServerTs(),

View File

@ -49,16 +49,17 @@ internal class SendEventWorker(context: Context, params: WorkerParameters)
val params = WorkerParamsFactory.fromData<Params>(inputData)
?: return Result.failure()

if (params.event.eventId == null) {
val event = params.event
if (event.eventId == null) {
return Result.failure()
}

val result = executeRequest<SendResponse> {
apiCall = roomAPI.send(
params.event.eventId,
event.eventId,
params.roomId,
params.event.type,
params.event.content
event.type,
event.content
)
}
result.flatMap { sendResponse ->
@ -69,6 +70,4 @@ internal class SendEventWorker(context: Context, params: WorkerParameters)
}
return result.fold({ Result.retry() }, { Result.success() })
}


}

View File

@ -22,7 +22,7 @@ import com.jakewharton.rxrelay2.BehaviorRelay
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.internal.session.room.media.MediaAttachment
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.matrix.rx.rx
import im.vector.riotredesign.core.platform.RiotViewModel
import im.vector.riotredesign.features.home.room.VisibleRoomStore
@ -81,14 +81,13 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
private fun handleSendMedia(action: RoomDetailActions.SendMedia) {
val attachment = action.mediaFiles.firstOrNull()
?.let {
MediaAttachment(
ContentAttachmentData(
it.size,
it.duration,
it.date,
it.height,
it.width,
it.name,
it.thumbnail,
it.path,
it.mimeType
)