mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-25 10:55:55 +03:00
Media upload : start handling progress.
This commit is contained in:
parent
c47eeb9cec
commit
c9658918ed
21 changed files with 445 additions and 200 deletions
|
@ -18,6 +18,7 @@ package im.vector.matrix.android.api.session
|
|||
|
||||
import androidx.annotation.MainThread
|
||||
import im.vector.matrix.android.api.auth.data.SessionParams
|
||||
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||
import im.vector.matrix.android.api.session.group.GroupService
|
||||
import im.vector.matrix.android.api.session.room.RoomService
|
||||
|
@ -51,6 +52,11 @@ interface Session : RoomService, GroupService, UserService {
|
|||
*/
|
||||
fun contentUrlResolver(): ContentUrlResolver
|
||||
|
||||
/**
|
||||
* Returns the ContentUploadProgressTracker associated with the session
|
||||
*/
|
||||
fun contentUploadProgressTracker(): ContentUploadStateTracker
|
||||
|
||||
/**
|
||||
* Add a listener to the session.
|
||||
* @param listener the listener to add.
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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
|
||||
|
||||
interface ContentUploadStateTracker {
|
||||
|
||||
fun track(eventId: String, updateListener: UpdateListener)
|
||||
|
||||
fun untrack(eventId: String, updateListener: UpdateListener)
|
||||
|
||||
interface UpdateListener {
|
||||
fun onUpdate(state: State)
|
||||
}
|
||||
|
||||
sealed class State {
|
||||
object Idle : State()
|
||||
data class ProgressData(val current: Long, val total: Long) : State()
|
||||
object Success : State()
|
||||
object Failure : State()
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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.network
|
||||
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.RequestBody
|
||||
import okio.Buffer
|
||||
import okio.BufferedSink
|
||||
import okio.ForwardingSink
|
||||
import okio.Okio
|
||||
import okio.Sink
|
||||
import java.io.IOException
|
||||
|
||||
internal class ProgressRequestBody(private val delegate: RequestBody,
|
||||
private val listener: Listener) : RequestBody() {
|
||||
|
||||
private lateinit var countingSink: CountingSink
|
||||
|
||||
override fun contentType(): MediaType? {
|
||||
return delegate.contentType()
|
||||
}
|
||||
|
||||
override fun contentLength(): Long {
|
||||
try {
|
||||
return delegate.contentLength()
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun writeTo(sink: BufferedSink) {
|
||||
countingSink = CountingSink(sink)
|
||||
val bufferedSink = Okio.buffer(countingSink)
|
||||
delegate.writeTo(bufferedSink)
|
||||
bufferedSink.flush()
|
||||
}
|
||||
|
||||
private inner class CountingSink(delegate: Sink) : ForwardingSink(delegate) {
|
||||
|
||||
private var bytesWritten: Long = 0
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun write(source: Buffer, byteCount: Long) {
|
||||
super.write(source, byteCount)
|
||||
bytesWritten += byteCount
|
||||
listener.onProgress(bytesWritten, contentLength())
|
||||
}
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun onProgress(current: Long, total: Long)
|
||||
}
|
||||
}
|
|
@ -22,6 +22,7 @@ import androidx.lifecycle.LiveData
|
|||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.auth.data.SessionParams
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||
import im.vector.matrix.android.api.session.group.Group
|
||||
import im.vector.matrix.android.api.session.group.GroupService
|
||||
|
@ -34,6 +35,7 @@ import im.vector.matrix.android.api.session.user.model.User
|
|||
import im.vector.matrix.android.internal.database.LiveEntityObserver
|
||||
import im.vector.matrix.android.internal.di.MatrixKoinComponent
|
||||
import im.vector.matrix.android.internal.di.MatrixKoinHolder
|
||||
import im.vector.matrix.android.internal.session.content.ContentModule
|
||||
import im.vector.matrix.android.internal.session.group.GroupModule
|
||||
import im.vector.matrix.android.internal.session.room.RoomModule
|
||||
import im.vector.matrix.android.internal.session.sync.SyncModule
|
||||
|
@ -59,6 +61,7 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
|
|||
private val userService by inject<UserService>()
|
||||
private val syncThread by inject<SyncThread>()
|
||||
private val contentUrlResolver by inject<ContentUrlResolver>()
|
||||
private val contentUploadProgressTracker by inject<ContentUploadStateTracker>()
|
||||
private var isOpen = false
|
||||
|
||||
@MainThread
|
||||
|
@ -71,7 +74,8 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
|
|||
val roomModule = RoomModule().definition
|
||||
val groupModule = GroupModule().definition
|
||||
val userModule = UserModule().definition
|
||||
MatrixKoinHolder.instance.loadModules(listOf(sessionModule, syncModule, roomModule, groupModule, userModule))
|
||||
val contentModule = ContentModule().definition
|
||||
MatrixKoinHolder.instance.loadModules(listOf(sessionModule, syncModule, roomModule, groupModule, userModule, contentModule))
|
||||
scope = getKoin().getOrCreateScope(SCOPE)
|
||||
if (!monarchy.isMonarchyThreadOpen) {
|
||||
monarchy.openManually()
|
||||
|
@ -98,6 +102,10 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
|
|||
return contentUrlResolver
|
||||
}
|
||||
|
||||
override fun contentUploadProgressTracker(): ContentUploadStateTracker {
|
||||
return contentUploadProgressTracker
|
||||
}
|
||||
|
||||
override fun addListener(listener: Session.Listener) {
|
||||
sessionListeners.addListener(listener)
|
||||
}
|
||||
|
|
|
@ -19,12 +19,10 @@ package im.vector.matrix.android.internal.session
|
|||
import android.content.Context
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.auth.data.SessionParams
|
||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||
import im.vector.matrix.android.api.session.group.GroupService
|
||||
import im.vector.matrix.android.api.session.room.RoomService
|
||||
import im.vector.matrix.android.api.session.user.UserService
|
||||
import im.vector.matrix.android.internal.database.LiveEntityObserver
|
||||
import im.vector.matrix.android.internal.session.content.DefaultContentUrlResolver
|
||||
import im.vector.matrix.android.internal.session.group.DefaultGroupService
|
||||
import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater
|
||||
import im.vector.matrix.android.internal.session.room.DefaultRoomService
|
||||
|
@ -110,10 +108,6 @@ internal class SessionModule(private val sessionParams: SessionParams) {
|
|||
SessionListeners()
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
DefaultContentUrlResolver(sessionParams.homeServerConnectionConfig) as ContentUrlResolver
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
val groupSummaryUpdater = GroupSummaryUpdater(get())
|
||||
val eventsPruner = EventsPruner(get())
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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 im.vector.matrix.android.api.auth.data.SessionParams
|
||||
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||
import im.vector.matrix.android.internal.session.DefaultSession
|
||||
import org.koin.dsl.module.module
|
||||
|
||||
internal class ContentModule {
|
||||
|
||||
val definition = module(override = true) {
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
DefaultContentUploadStateTracker() as ContentUploadStateTracker
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
ContentUploader(get(), get(), get<ContentUploadStateTracker>() as DefaultContentUploadStateTracker)
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
val sessionParams = get<SessionParams>()
|
||||
DefaultContentUrlResolver(sessionParams.homeServerConnectionConfig) as ContentUrlResolver
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -21,6 +21,7 @@ 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 im.vector.matrix.android.internal.network.ProgressRequestBody
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.OkHttpClient
|
||||
|
@ -31,12 +32,13 @@ import java.io.IOException
|
|||
|
||||
|
||||
internal class ContentUploader(private val okHttpClient: OkHttpClient,
|
||||
private val sessionParams: SessionParams) {
|
||||
private val sessionParams: SessionParams,
|
||||
private val contentUploadProgressTracker: DefaultContentUploadStateTracker) {
|
||||
|
||||
private val moshi = MoshiProvider.providesMoshi()
|
||||
private val responseAdapter = moshi.adapter(ContentUploadResponse::class.java)
|
||||
|
||||
fun uploadFile(attachment: ContentAttachmentData): Try<ContentUploadResponse> {
|
||||
fun uploadFile(eventId: String, attachment: ContentAttachmentData): Try<ContentUploadResponse> {
|
||||
if (attachment.path == null || attachment.mimeType == null) {
|
||||
return raise(RuntimeException())
|
||||
}
|
||||
|
@ -55,12 +57,18 @@ internal class ContentUploader(private val okHttpClient: OkHttpClient,
|
|||
MediaType.parse(attachment.mimeType),
|
||||
file
|
||||
)
|
||||
val progressRequestBody = ProgressRequestBody(requestBody, object : ProgressRequestBody.Listener {
|
||||
override fun onProgress(current: Long, total: Long) {
|
||||
contentUploadProgressTracker.setProgress(eventId, current, total)
|
||||
}
|
||||
})
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(httpUrl)
|
||||
.post(requestBody)
|
||||
.post(progressRequestBody)
|
||||
.build()
|
||||
|
||||
return Try {
|
||||
val result = Try {
|
||||
okHttpClient.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
throw IOException()
|
||||
|
@ -72,5 +80,11 @@ internal class ContentUploader(private val okHttpClient: OkHttpClient,
|
|||
}
|
||||
}
|
||||
}
|
||||
if (result.isFailure()) {
|
||||
contentUploadProgressTracker.setFailure(eventId)
|
||||
} else {
|
||||
contentUploadProgressTracker.setSuccess(eventId)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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.os.Handler
|
||||
import android.os.Looper
|
||||
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
||||
|
||||
internal class DefaultContentUploadStateTracker : ContentUploadStateTracker {
|
||||
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
private val progressByEvent = mutableMapOf<String, ContentUploadStateTracker.State>()
|
||||
private val listenersByEvent = mutableMapOf<String, MutableList<ContentUploadStateTracker.UpdateListener>>()
|
||||
|
||||
override fun track(eventId: String, updateListener: ContentUploadStateTracker.UpdateListener) {
|
||||
val listeners = listenersByEvent[eventId] ?: ArrayList()
|
||||
listeners.add(updateListener)
|
||||
listenersByEvent[eventId] = listeners
|
||||
val currentState = progressByEvent[eventId] ?: ContentUploadStateTracker.State.Idle
|
||||
mainHandler.post { updateListener.onUpdate(currentState) }
|
||||
}
|
||||
|
||||
override fun untrack(eventId: String, updateListener: ContentUploadStateTracker.UpdateListener) {
|
||||
listenersByEvent[eventId]?.apply {
|
||||
remove(updateListener)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun setFailure(eventId: String) {
|
||||
val failure = ContentUploadStateTracker.State.Failure
|
||||
updateState(eventId, failure)
|
||||
}
|
||||
|
||||
internal fun setSuccess(eventId: String) {
|
||||
val success = ContentUploadStateTracker.State.Success
|
||||
updateState(eventId, success)
|
||||
}
|
||||
|
||||
internal fun setProgress(eventId: String, current: Long, total: Long) {
|
||||
val progressData = ContentUploadStateTracker.State.ProgressData(current, total)
|
||||
updateState(eventId, progressData)
|
||||
}
|
||||
|
||||
private fun updateState(eventId: String, state: ContentUploadStateTracker.State) {
|
||||
progressByEvent[eventId] = state
|
||||
mainHandler.post {
|
||||
listenersByEvent[eventId]?.also { listeners ->
|
||||
listeners.forEach { it.onUpdate(state) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -17,7 +17,7 @@
|
|||
package im.vector.matrix.android.internal.session.content
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.Worker
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||
|
@ -32,7 +32,7 @@ import im.vector.matrix.android.internal.util.WorkerParamsFactory
|
|||
import org.koin.standalone.inject
|
||||
|
||||
internal class UploadContentWorker(context: Context, params: WorkerParameters)
|
||||
: Worker(context, params), MatrixKoinComponent {
|
||||
: CoroutineWorker(context, params), MatrixKoinComponent {
|
||||
|
||||
private val mediaUploader by inject<ContentUploader>()
|
||||
|
||||
|
@ -43,12 +43,15 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters)
|
|||
val attachment: ContentAttachmentData
|
||||
)
|
||||
|
||||
override fun doWork(): Result {
|
||||
override suspend fun doWork(): Result {
|
||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||
?: return Result.failure()
|
||||
|
||||
if (params.event.eventId == null) {
|
||||
return Result.failure()
|
||||
}
|
||||
return mediaUploader
|
||||
.uploadFile(params.attachment)
|
||||
.uploadFile(params.event.eventId, params.attachment)
|
||||
.fold({ handleFailure() }, { handleSuccess(params, it) })
|
||||
}
|
||||
|
||||
|
|
|
@ -17,13 +17,16 @@
|
|||
package im.vector.matrix.android.internal.session.room
|
||||
|
||||
import im.vector.matrix.android.internal.session.DefaultSession
|
||||
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.LocalEchoEventFactory
|
||||
import im.vector.matrix.android.internal.session.room.timeline.*
|
||||
import im.vector.matrix.android.internal.session.room.timeline.DefaultGetContextOfEventTask
|
||||
import im.vector.matrix.android.internal.session.room.timeline.DefaultPaginationTask
|
||||
import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventTask
|
||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationTask
|
||||
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor
|
||||
import org.koin.dsl.module.module
|
||||
import retrofit2.Retrofit
|
||||
|
||||
|
@ -61,10 +64,6 @@ class RoomModule {
|
|||
LocalEchoEventFactory(get())
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
ContentUploader(get(), get())
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
RoomFactory(get(), get(), get(), get(), get(), get(), get())
|
||||
}
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
/*
|
||||
*
|
||||
* * 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.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 path: String? = null,
|
||||
val mimeType: String? = null
|
||||
) : Parcelable
|
|
@ -1,69 +0,0 @@
|
|||
/*
|
||||
*
|
||||
* * 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.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 java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
|
||||
internal class MediaUploader(private val okHttpClient: OkHttpClient,
|
||||
private val sessionParams: SessionParams) {
|
||||
|
||||
fun uploadFile(attachment: ContentAttachmentData): Try<String> {
|
||||
if (attachment.path == null || attachment.mimeType == null) {
|
||||
return Try.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 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(httpUrl)
|
||||
.post(requestBody)
|
||||
.build()
|
||||
|
||||
return okHttpClient.newCall(request).execute().use { response ->
|
||||
if (response.isSuccessful) {
|
||||
Try.raise(IOException(""))
|
||||
} else {
|
||||
Try.just(response.message())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
/*
|
||||
*
|
||||
* * 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.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<ContentUploader>()
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class Params(
|
||||
val attachment: ContentAttachmentData
|
||||
)
|
||||
|
||||
override fun doWork(): Result {
|
||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||
?: return Result.failure()
|
||||
|
||||
return mediaUploader
|
||||
.uploadFile(params.attachment)
|
||||
.fold({ Result.retry() }, { Result.success() })
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -23,7 +23,11 @@ import im.vector.matrix.android.api.permalinks.MatrixLinkify
|
|||
import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.message.*
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageEmoteContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageNoticeContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.epoxy.RiotEpoxyModel
|
||||
|
@ -32,7 +36,13 @@ import im.vector.riotredesign.core.resources.ColorProvider
|
|||
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.*
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.DefaultItem
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.DefaultItem_
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageImageItem
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageImageItem_
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageTextItem
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageTextItem_
|
||||
import im.vector.riotredesign.features.html.EventHtmlRenderer
|
||||
import im.vector.riotredesign.features.media.MediaContentRenderer
|
||||
import me.gujun.android.span.span
|
||||
|
@ -47,6 +57,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||
callback: TimelineEventController.Callback?
|
||||
): RiotEpoxyModel<*>? {
|
||||
|
||||
val eventId = event.root.eventId ?: return null
|
||||
val roomMember = event.roomMember
|
||||
val nextRoomMember = nextEvent?.roomMember
|
||||
|
||||
|
@ -54,12 +65,12 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||
val nextDate = nextEvent?.root?.localDateTime()
|
||||
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
|
||||
val isNextMessageReceivedMoreThanOneHourAgo = nextDate?.isBefore(date.minusMinutes(60))
|
||||
?: false
|
||||
?: false
|
||||
|
||||
val showInformation = addDaySeparator
|
||||
|| nextRoomMember != roomMember
|
||||
|| nextEvent?.root?.type != EventType.MESSAGE
|
||||
|| isNextMessageReceivedMoreThanOneHourAgo
|
||||
|| nextRoomMember != roomMember
|
||||
|| nextEvent?.root?.type != EventType.MESSAGE
|
||||
|| isNextMessageReceivedMoreThanOneHourAgo
|
||||
|
||||
val messageContent: MessageContent = event.root.content.toModel() ?: return null
|
||||
val time = timelineDateFormatter.formatMessageHour(date)
|
||||
|
@ -69,7 +80,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||
|
||||
return when (messageContent) {
|
||||
is MessageTextContent -> buildTextMessageItem(messageContent, informationData, callback)
|
||||
is MessageImageContent -> buildImageMessageItem(messageContent, informationData, callback)
|
||||
is MessageImageContent -> buildImageMessageItem(eventId, messageContent, informationData, callback)
|
||||
is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, callback)
|
||||
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, callback)
|
||||
else -> buildNotHandledMessageItem(messageContent)
|
||||
|
@ -81,7 +92,8 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||
return DefaultItem_().text(text)
|
||||
}
|
||||
|
||||
private fun buildImageMessageItem(messageContent: MessageImageContent,
|
||||
private fun buildImageMessageItem(eventId: String,
|
||||
messageContent: MessageImageContent,
|
||||
informationData: MessageInformationData,
|
||||
callback: TimelineEventController.Callback?): MessageImageItem? {
|
||||
|
||||
|
@ -97,6 +109,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||
orientation = messageContent.info?.orientation
|
||||
)
|
||||
return MessageImageItem_()
|
||||
.eventId(eventId)
|
||||
.informationData(informationData)
|
||||
.mediaData(data)
|
||||
.clickListener { view -> callback?.onMediaClicked(data, view) }
|
||||
|
@ -107,10 +120,10 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||
callback: TimelineEventController.Callback?): MessageTextItem? {
|
||||
|
||||
val bodyToUse = messageContent.formattedBody
|
||||
?.let {
|
||||
htmlRenderer.render(it)
|
||||
}
|
||||
?: messageContent.body
|
||||
?.let {
|
||||
htmlRenderer.render(it)
|
||||
}
|
||||
?: messageContent.body
|
||||
|
||||
val linkifiedBody = linkifyBody(bodyToUse, callback)
|
||||
return MessageTextItem_()
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* 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.riotredesign.features.home.room.detail.timeline.helper
|
||||
|
||||
import android.content.Context
|
||||
import android.text.format.Formatter
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import im.vector.matrix.android.api.Matrix
|
||||
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
||||
import im.vector.riotredesign.R
|
||||
|
||||
object ContentUploadStateTrackerBinder {
|
||||
|
||||
private val updateListeners = mutableMapOf<String, ContentUploadStateTracker.UpdateListener>()
|
||||
|
||||
fun bind(eventId: String, progressLayout: ViewGroup) {
|
||||
Matrix.getInstance().currentSession?.also { session ->
|
||||
val uploadStateTracker = session.contentUploadProgressTracker()
|
||||
val updateListener = ContentMediaProgressUpdater(progressLayout)
|
||||
updateListeners[eventId] = updateListener
|
||||
uploadStateTracker.track(eventId, updateListener)
|
||||
}
|
||||
}
|
||||
|
||||
fun unbind(eventId: String) {
|
||||
Matrix.getInstance().currentSession?.also { session ->
|
||||
val uploadStateTracker = session.contentUploadProgressTracker()
|
||||
updateListeners[eventId]?.also {
|
||||
uploadStateTracker.untrack(eventId, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup) : ContentUploadStateTracker.UpdateListener {
|
||||
|
||||
override fun onUpdate(state: ContentUploadStateTracker.State) {
|
||||
when (state) {
|
||||
is ContentUploadStateTracker.State.Idle,
|
||||
is ContentUploadStateTracker.State.Failure,
|
||||
is ContentUploadStateTracker.State.Success -> hideProgress()
|
||||
is ContentUploadStateTracker.State.ProgressData -> showProgress(state)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideProgress() {
|
||||
progressLayout.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun showProgress(state: ContentUploadStateTracker.State.ProgressData) {
|
||||
progressLayout.visibility = View.VISIBLE
|
||||
val percent = 100L * (state.current.toFloat() / state.total.toFloat())
|
||||
val progressBar = progressLayout.findViewById<ProgressBar>(R.id.mediaProgressBar)
|
||||
val progressTextView = progressLayout.findViewById<TextView>(R.id.mediaProgressTextView)
|
||||
progressBar?.progress = percent.toInt()
|
||||
progressTextView?.text = formatStats(progressLayout.context, state.current, state.total)
|
||||
}
|
||||
|
||||
private fun formatStats(context: Context, current: Long, total: Long): String {
|
||||
return "${Formatter.formatShortFileSize(context, current)} / ${Formatter.formatShortFileSize(context, total)}"
|
||||
}
|
||||
|
||||
}
|
|
@ -28,6 +28,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : RiotEpoxyModel<H>() {
|
|||
abstract val informationData: MessageInformationData
|
||||
|
||||
override fun bind(holder: H) {
|
||||
super.bind(holder)
|
||||
if (informationData.showInformation) {
|
||||
holder.avatarImageView.visibility = View.VISIBLE
|
||||
holder.memberNameView.visibility = View.VISIBLE
|
||||
|
|
|
@ -17,30 +17,40 @@
|
|||
package im.vector.riotredesign.features.home.room.detail.timeline.item
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
|
||||
import im.vector.riotredesign.features.media.MediaContentRenderer
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_image_message)
|
||||
abstract class MessageImageItem : AbsMessageItem<MessageImageItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute lateinit var mediaData: MediaContentRenderer.Data
|
||||
@EpoxyAttribute lateinit var eventId: String
|
||||
@EpoxyAttribute override lateinit var informationData: MessageInformationData
|
||||
@EpoxyAttribute var clickListener: View.OnClickListener? = null
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
MediaContentRenderer.render(mediaData, MediaContentRenderer.Mode.THUMBNAIL, holder.imageView)
|
||||
ContentUploadStateTrackerBinder.bind(eventId, holder.progressLayout)
|
||||
holder.imageView.setOnClickListener(clickListener)
|
||||
}
|
||||
|
||||
override fun unbind(holder: Holder) {
|
||||
ContentUploadStateTrackerBinder.unbind(eventId)
|
||||
super.unbind(holder)
|
||||
}
|
||||
|
||||
class Holder : AbsMessageItem.Holder() {
|
||||
override val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
|
||||
override val memberNameView by bind<TextView>(R.id.messageMemberNameView)
|
||||
override val timeView by bind<TextView>(R.id.messageTimeView)
|
||||
val progressLayout by bind<ViewGroup>(R.id.messageImageUploadProgressLayout)
|
||||
val imageView by bind<ImageView>(R.id.messageImageView)
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ import im.vector.matrix.android.api.Matrix
|
|||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||
import im.vector.riotredesign.core.glide.GlideApp
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import java.io.File
|
||||
|
||||
object MediaContentRenderer {
|
||||
|
||||
|
@ -38,7 +39,12 @@ object MediaContentRenderer {
|
|||
val maxWidth: Int,
|
||||
val orientation: Int?,
|
||||
val rotation: Int?
|
||||
) : Parcelable
|
||||
) : Parcelable {
|
||||
|
||||
fun isLocalFile(): Boolean {
|
||||
return url != null && File(url).exists()
|
||||
}
|
||||
}
|
||||
|
||||
enum class Mode {
|
||||
FULL_SIZE,
|
||||
|
@ -51,11 +57,11 @@ object MediaContentRenderer {
|
|||
imageView.layoutParams.width = width
|
||||
val contentUrlResolver = Matrix.getInstance().currentSession!!.contentUrlResolver()
|
||||
val resolvedUrl = when (mode) {
|
||||
Mode.FULL_SIZE -> contentUrlResolver.resolveFullSize(data.url)
|
||||
Mode.THUMBNAIL -> contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE)
|
||||
}
|
||||
//Fallback to base url
|
||||
?: data.url
|
||||
Mode.FULL_SIZE -> contentUrlResolver.resolveFullSize(data.url)
|
||||
Mode.THUMBNAIL -> contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE)
|
||||
}
|
||||
//Fallback to base url
|
||||
?: data.url
|
||||
|
||||
GlideApp
|
||||
.with(imageView)
|
||||
|
@ -67,12 +73,16 @@ object MediaContentRenderer {
|
|||
fun render(data: Data, imageView: BigImageView) {
|
||||
val (width, height) = processSize(data, Mode.THUMBNAIL)
|
||||
val contentUrlResolver = Matrix.getInstance().currentSession!!.contentUrlResolver()
|
||||
val fullSize = contentUrlResolver.resolveFullSize(data.url)
|
||||
val thumbnail = contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE)
|
||||
imageView.showImage(
|
||||
Uri.parse(thumbnail ?: data.url),
|
||||
Uri.parse(fullSize ?: data.url)
|
||||
)
|
||||
if (data.isLocalFile()) {
|
||||
imageView.showImage(Uri.parse(data.url))
|
||||
} else {
|
||||
val fullSize = contentUrlResolver.resolveFullSize(data.url)
|
||||
val thumbnail = contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE)
|
||||
imageView.showImage(
|
||||
Uri.parse(thumbnail),
|
||||
Uri.parse(fullSize)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun processSize(data: Data, mode: Mode): Pair<Int, Int> {
|
||||
|
|
BIN
vector/src/main/res/drawable-mdpi/vector_cancel_upload_download.png
Executable file
BIN
vector/src/main/res/drawable-mdpi/vector_cancel_upload_download.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 309 B |
|
@ -59,10 +59,23 @@
|
|||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginRight="32dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/messageMemberNameView" />
|
||||
|
||||
<include
|
||||
android:id="@+id/messageImageUploadProgressLayout"
|
||||
layout="@layout/media_upload_download_progress_layout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="46dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="@+id/messageImageView"
|
||||
app:layout_constraintStart_toStartOf="@+id/messageImageView"
|
||||
app:layout_constraintTop_toBottomOf="@+id/messageImageView"
|
||||
tools:visibility="visible" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,28 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/mediaProgressTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:textSize="12sp"
|
||||
tools:text="Information" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/mediaProgressBar"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="20dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:max="100"
|
||||
android:min="0"
|
||||
android:progress="0"
|
||||
tools:progress="45" />
|
||||
|
||||
</LinearLayout>
|
Loading…
Reference in a new issue