Media: start to play with uploading media

This commit is contained in:
ganfra
2019-04-03 22:54:48 +02:00
parent 96a67a44ac
commit 18591d0287
16 changed files with 544 additions and 52 deletions

View File

@ -1,6 +1,7 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'realm-android'
apply plugin: 'okreplay'
@ -19,6 +20,10 @@ repositories {
jcenter()
}
androidExtensions {
experimental = true
}
android {
compileSdkVersion 28
testOptions.unitTests.includeAndroidResources = true

View File

@ -19,6 +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
/**
* This interface defines methods to send events in a room. It's implemented at the room level.
@ -33,5 +34,6 @@ interface SendService {
*/
fun sendTextMessage(text: String, callback: MatrixCallback<Event>): Cancelable
fun sendMedia(attachment: MediaAttachment, callback: MatrixCallback<Event>): Cancelable
}

View File

@ -21,7 +21,7 @@ import im.vector.matrix.android.api.session.content.ContentUrlResolver
private const val MATRIX_CONTENT_URI_SCHEME = "mxc://"
private const val URI_PREFIX_CONTENT_API = "/_matrix/media/v1/"
internal const val URI_PREFIX_CONTENT_API = "/_matrix/media/v1/"
internal class DefaultContentUrlResolver(private val homeServerConnectionConfig: HomeServerConnectionConfig) : ContentUrlResolver {

View File

@ -17,7 +17,6 @@
package im.vector.matrix.android.internal.session.room
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTask
import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor
@ -33,7 +32,6 @@ import im.vector.matrix.android.internal.task.TaskExecutor
internal class RoomFactory(private val loadRoomMembersTask: LoadRoomMembersTask,
private val monarchy: Monarchy,
private val credentials: Credentials,
private val paginationTask: PaginationTask,
private val contextOfEventTask: GetContextOfEventTask,
private val setReadMarkersTask: SetReadMarkersTask,

View File

@ -17,6 +17,7 @@
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.room.members.DefaultLoadRoomMembersTask
import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTask
import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask
@ -61,7 +62,11 @@ class RoomModule {
}
scope(DefaultSession.SCOPE) {
RoomFactory(get(), get(), get(), get(), get(), get(), get(), get())
MediaUploader(get(), get())
}
scope(DefaultSession.SCOPE) {
RoomFactory(get(), get(), get(), get(), get(), get(), get())
}
}

View File

@ -0,0 +1,36 @@
/*
*
* * 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.room.media
import android.net.Uri
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
@Parcelize
data class MediaAttachment(
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 thumbnail: Uri? = null,
val path: String? = null,
val mimeType: String? = null
) : Parcelable

View File

@ -0,0 +1,60 @@
/*
*
* * 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.room.media
import arrow.core.Try
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.internal.session.content.URI_PREFIX_CONTENT_API
import okhttp3.MediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import java.io.File
import java.io.IOException
internal class MediaUploader(private val okHttpClient: OkHttpClient,
private val sessionParams: SessionParams) {
fun uploadFile(attachment: MediaAttachment): 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)
// create RequestBody instance from file
val requestFile = RequestBody.create(
MediaType.parse(attachment.mimeType),
file
)
val request = Request.Builder()
.url(urlString)
.post(requestFile)
.build()
return okHttpClient.newCall(request).execute().use { response ->
if (response.isSuccessful) {
Try.raise(IOException(""))
} else {
Try.just(response.message())
}
}
}
}

View File

@ -0,0 +1,49 @@
/*
*
* * 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.room.media
import android.content.Context
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.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>()
@JsonClass(generateAdapter = true)
internal data class Params(
val attachment: MediaAttachment
)
override fun doWork(): Result {
val params = WorkerParamsFactory.fromData<Params>(inputData)
?: return Result.failure()
return mediaUploader
.uploadFile(params.attachment)
.fold({ Result.retry() }, { Result.success() })
}
}

View File

@ -25,6 +25,8 @@ 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.room.timeline.PaginationDirection
import im.vector.matrix.android.internal.util.CancelableWork
import im.vector.matrix.android.internal.util.WorkerParamsFactory
@ -32,39 +34,70 @@ import im.vector.matrix.android.internal.util.tryTransactionAsync
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 monarchy: Monarchy) : SendService {
private val sendConstraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
override fun sendTextMessage(text: String, callback: MatrixCallback<Event>): Cancelable {
val event = eventFactory.createTextEvent(roomId, text)
monarchy.tryTransactionAsync { realm ->
val chunkEntity = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)
?: return@tryTransactionAsync
chunkEntity.add(roomId, event, PaginationDirection.FORWARDS)
}
val sendContentWorkerParams = SendEventWorker.Params(roomId, event)
val workData = WorkerParamsFactory.toData(sendContentWorkerParams)
val sendWork = OneTimeWorkRequestBuilder<SendEventWorker>()
.setConstraints(sendConstraints)
.setInputData(workData)
.setBackoffCriteria(BackoffPolicy.LINEAR, 10_000, TimeUnit.MILLISECONDS)
.build()
saveLiveEvent(event)
val sendWork = createSendEventWork(event)
WorkManager.getInstance()
.beginUniqueWork(SEND_WORK, ExistingWorkPolicy.APPEND, sendWork)
.enqueue()
return CancelableWork(sendWork.id)
}
override fun sendMedia(attachment: MediaAttachment, 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 sendWork = createSendEventWork(event)
WorkManager.getInstance()
.beginUniqueWork(SEND_WORK, ExistingWorkPolicy.APPEND, uploadWork)
.then(sendWork)
.enqueue()
return CancelableWork(sendWork.id)
}
private fun saveLiveEvent(event: Event) {
monarchy.tryTransactionAsync { realm ->
val chunkEntity = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)
?: return@tryTransactionAsync
chunkEntity.add(roomId, event, PaginationDirection.FORWARDS)
}
}
private fun createSendEventWork(event: Event): OneTimeWorkRequest {
val sendContentWorkerParams = SendEventWorker.Params(roomId, event)
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
return OneTimeWorkRequestBuilder<SendEventWorker>()
.setConstraints(WORK_CONSTRAINTS)
.setInputData(sendWorkData)
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
.build()
}
private fun createUploadMediaWork(attachment: MediaAttachment): OneTimeWorkRequest {
val uploadMediaWorkerParams = UploadMediaWorker.Params(attachment)
val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams)
return OneTimeWorkRequestBuilder<UploadMediaWorker>()
.setConstraints(WORK_CONSTRAINTS)
.setInputData(uploadWorkData)
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
.build()
}
}

View File

@ -17,27 +17,45 @@
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.events.model.Content
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.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.di.MoshiProvider
import im.vector.matrix.android.internal.session.room.media.MediaAttachment
internal class EventFactory(private val credentials: Credentials) {
private val moshi = MoshiProvider.providesMoshi()
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 {
val content = MessageImageContent(
type = MessageType.MSGTYPE_IMAGE,
body = attachment.name ?: "image",
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 {
return Event(
roomId = roomId,
originServerTs = dummyOriginServerTs(),
sender = credentials.userId,
eventId = dummyEventId(roomId),
type = EventType.MESSAGE,
content = toContent(content)
content = content.toContent()
)
}
@ -48,13 +66,4 @@ internal class EventFactory(private val credentials: Credentials) {
private fun dummyEventId(roomId: String): String {
return roomId + "-" + dummyOriginServerTs()
}
@Suppress("UNCHECKED_CAST")
private inline fun <reified T> toContent(data: T?): Content? {
val moshiAdapter = moshi.adapter(T::class.java)
val jsonValue = moshiAdapter.toJsonValue(data)
return jsonValue as? Content?
}
}