Media upload : start handling progress.

This commit is contained in:
ganfra 2019-04-05 18:21:45 +02:00
parent c47eeb9cec
commit c9658918ed
21 changed files with 445 additions and 200 deletions

View file

@ -18,6 +18,7 @@ package im.vector.matrix.android.api.session
import androidx.annotation.MainThread import androidx.annotation.MainThread
import im.vector.matrix.android.api.auth.data.SessionParams 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.content.ContentUrlResolver
import im.vector.matrix.android.api.session.group.GroupService 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.room.RoomService
@ -51,6 +52,11 @@ interface Session : RoomService, GroupService, UserService {
*/ */
fun contentUrlResolver(): ContentUrlResolver fun contentUrlResolver(): ContentUrlResolver
/**
* Returns the ContentUploadProgressTracker associated with the session
*/
fun contentUploadProgressTracker(): ContentUploadStateTracker
/** /**
* Add a listener to the session. * Add a listener to the session.
* @param listener the listener to add. * @param listener the listener to add.

View file

@ -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()
}
}

View file

@ -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)
}
}

View file

@ -22,6 +22,7 @@ import androidx.lifecycle.LiveData
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.session.Session 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.content.ContentUrlResolver
import im.vector.matrix.android.api.session.group.Group import im.vector.matrix.android.api.session.group.Group
import im.vector.matrix.android.api.session.group.GroupService 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.database.LiveEntityObserver
import im.vector.matrix.android.internal.di.MatrixKoinComponent import im.vector.matrix.android.internal.di.MatrixKoinComponent
import im.vector.matrix.android.internal.di.MatrixKoinHolder 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.group.GroupModule
import im.vector.matrix.android.internal.session.room.RoomModule import im.vector.matrix.android.internal.session.room.RoomModule
import im.vector.matrix.android.internal.session.sync.SyncModule 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 userService by inject<UserService>()
private val syncThread by inject<SyncThread>() private val syncThread by inject<SyncThread>()
private val contentUrlResolver by inject<ContentUrlResolver>() private val contentUrlResolver by inject<ContentUrlResolver>()
private val contentUploadProgressTracker by inject<ContentUploadStateTracker>()
private var isOpen = false private var isOpen = false
@MainThread @MainThread
@ -71,7 +74,8 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
val roomModule = RoomModule().definition val roomModule = RoomModule().definition
val groupModule = GroupModule().definition val groupModule = GroupModule().definition
val userModule = UserModule().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) scope = getKoin().getOrCreateScope(SCOPE)
if (!monarchy.isMonarchyThreadOpen) { if (!monarchy.isMonarchyThreadOpen) {
monarchy.openManually() monarchy.openManually()
@ -98,6 +102,10 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
return contentUrlResolver return contentUrlResolver
} }
override fun contentUploadProgressTracker(): ContentUploadStateTracker {
return contentUploadProgressTracker
}
override fun addListener(listener: Session.Listener) { override fun addListener(listener: Session.Listener) {
sessionListeners.addListener(listener) sessionListeners.addListener(listener)
} }

View file

@ -19,12 +19,10 @@ package im.vector.matrix.android.internal.session
import android.content.Context import android.content.Context
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.auth.data.SessionParams 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.group.GroupService
import im.vector.matrix.android.api.session.room.RoomService import im.vector.matrix.android.api.session.room.RoomService
import im.vector.matrix.android.api.session.user.UserService import im.vector.matrix.android.api.session.user.UserService
import im.vector.matrix.android.internal.database.LiveEntityObserver 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.DefaultGroupService
import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater
import im.vector.matrix.android.internal.session.room.DefaultRoomService import im.vector.matrix.android.internal.session.room.DefaultRoomService
@ -110,10 +108,6 @@ internal class SessionModule(private val sessionParams: SessionParams) {
SessionListeners() SessionListeners()
} }
scope(DefaultSession.SCOPE) {
DefaultContentUrlResolver(sessionParams.homeServerConnectionConfig) as ContentUrlResolver
}
scope(DefaultSession.SCOPE) { scope(DefaultSession.SCOPE) {
val groupSummaryUpdater = GroupSummaryUpdater(get()) val groupSummaryUpdater = GroupSummaryUpdater(get())
val eventsPruner = EventsPruner(get()) val eventsPruner = EventsPruner(get())

View file

@ -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
}
}
}

View file

@ -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.auth.data.SessionParams
import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.network.ProgressRequestBody
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.MediaType import okhttp3.MediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -31,12 +32,13 @@ import java.io.IOException
internal class ContentUploader(private val okHttpClient: OkHttpClient, 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 moshi = MoshiProvider.providesMoshi()
private val responseAdapter = moshi.adapter(ContentUploadResponse::class.java) 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) { if (attachment.path == null || attachment.mimeType == null) {
return raise(RuntimeException()) return raise(RuntimeException())
} }
@ -55,12 +57,18 @@ internal class ContentUploader(private val okHttpClient: OkHttpClient,
MediaType.parse(attachment.mimeType), MediaType.parse(attachment.mimeType),
file file
) )
val progressRequestBody = ProgressRequestBody(requestBody, object : ProgressRequestBody.Listener {
override fun onProgress(current: Long, total: Long) {
contentUploadProgressTracker.setProgress(eventId, current, total)
}
})
val request = Request.Builder() val request = Request.Builder()
.url(httpUrl) .url(httpUrl)
.post(requestBody) .post(progressRequestBody)
.build() .build()
return Try { val result = Try {
okHttpClient.newCall(request).execute().use { response -> okHttpClient.newCall(request).execute().use { response ->
if (!response.isSuccessful) { if (!response.isSuccessful) {
throw IOException() 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
} }
} }

View file

@ -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) }
}
}
}
}

View file

@ -17,7 +17,7 @@
package im.vector.matrix.android.internal.session.content package im.vector.matrix.android.internal.session.content
import android.content.Context import android.content.Context
import androidx.work.Worker import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.content.ContentAttachmentData 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 import org.koin.standalone.inject
internal class UploadContentWorker(context: Context, params: WorkerParameters) internal class UploadContentWorker(context: Context, params: WorkerParameters)
: Worker(context, params), MatrixKoinComponent { : CoroutineWorker(context, params), MatrixKoinComponent {
private val mediaUploader by inject<ContentUploader>() private val mediaUploader by inject<ContentUploader>()
@ -43,12 +43,15 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters)
val attachment: ContentAttachmentData val attachment: ContentAttachmentData
) )
override fun doWork(): Result { override suspend fun doWork(): Result {
val params = WorkerParamsFactory.fromData<Params>(inputData) val params = WorkerParamsFactory.fromData<Params>(inputData)
?: return Result.failure() ?: return Result.failure()
if (params.event.eventId == null) {
return Result.failure()
}
return mediaUploader return mediaUploader
.uploadFile(params.attachment) .uploadFile(params.event.eventId, params.attachment)
.fold({ handleFailure() }, { handleSuccess(params, it) }) .fold({ handleFailure() }, { handleSuccess(params, it) })
} }

View file

@ -17,13 +17,16 @@
package im.vector.matrix.android.internal.session.room package im.vector.matrix.android.internal.session.room
import im.vector.matrix.android.internal.session.DefaultSession 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.DefaultLoadRoomMembersTask
import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTask 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.DefaultSetReadMarkersTask
import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask 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.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 org.koin.dsl.module.module
import retrofit2.Retrofit import retrofit2.Retrofit
@ -61,10 +64,6 @@ class RoomModule {
LocalEchoEventFactory(get()) LocalEchoEventFactory(get())
} }
scope(DefaultSession.SCOPE) {
ContentUploader(get(), get())
}
scope(DefaultSession.SCOPE) { scope(DefaultSession.SCOPE) {
RoomFactory(get(), get(), get(), get(), get(), get(), get()) RoomFactory(get(), get(), get(), get(), get(), get(), get())
} }

View file

@ -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

View file

@ -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())
}
}
}
}

View file

@ -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() })
}
}

View file

@ -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.permalinks.MatrixPermalinkSpan
import im.vector.matrix.android.api.session.events.model.EventType 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.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.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.RiotEpoxyModel 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.TimelineEventController
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter 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.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.html.EventHtmlRenderer
import im.vector.riotredesign.features.media.MediaContentRenderer import im.vector.riotredesign.features.media.MediaContentRenderer
import me.gujun.android.span.span import me.gujun.android.span.span
@ -47,6 +57,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
callback: TimelineEventController.Callback? callback: TimelineEventController.Callback?
): RiotEpoxyModel<*>? { ): RiotEpoxyModel<*>? {
val eventId = event.root.eventId ?: return null
val roomMember = event.roomMember val roomMember = event.roomMember
val nextRoomMember = nextEvent?.roomMember val nextRoomMember = nextEvent?.roomMember
@ -69,7 +80,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
return when (messageContent) { return when (messageContent) {
is MessageTextContent -> buildTextMessageItem(messageContent, informationData, callback) 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 MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, callback)
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, callback) is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, callback)
else -> buildNotHandledMessageItem(messageContent) else -> buildNotHandledMessageItem(messageContent)
@ -81,7 +92,8 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
return DefaultItem_().text(text) return DefaultItem_().text(text)
} }
private fun buildImageMessageItem(messageContent: MessageImageContent, private fun buildImageMessageItem(eventId: String,
messageContent: MessageImageContent,
informationData: MessageInformationData, informationData: MessageInformationData,
callback: TimelineEventController.Callback?): MessageImageItem? { callback: TimelineEventController.Callback?): MessageImageItem? {
@ -97,6 +109,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
orientation = messageContent.info?.orientation orientation = messageContent.info?.orientation
) )
return MessageImageItem_() return MessageImageItem_()
.eventId(eventId)
.informationData(informationData) .informationData(informationData)
.mediaData(data) .mediaData(data)
.clickListener { view -> callback?.onMediaClicked(data, view) } .clickListener { view -> callback?.onMediaClicked(data, view) }

View file

@ -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)}"
}
}

View file

@ -28,6 +28,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : RiotEpoxyModel<H>() {
abstract val informationData: MessageInformationData abstract val informationData: MessageInformationData
override fun bind(holder: H) { override fun bind(holder: H) {
super.bind(holder)
if (informationData.showInformation) { if (informationData.showInformation) {
holder.avatarImageView.visibility = View.VISIBLE holder.avatarImageView.visibility = View.VISIBLE
holder.memberNameView.visibility = View.VISIBLE holder.memberNameView.visibility = View.VISIBLE

View file

@ -17,30 +17,40 @@
package im.vector.riotredesign.features.home.room.detail.timeline.item package im.vector.riotredesign.features.home.room.detail.timeline.item
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
import im.vector.riotredesign.features.media.MediaContentRenderer import im.vector.riotredesign.features.media.MediaContentRenderer
@EpoxyModelClass(layout = R.layout.item_timeline_event_image_message) @EpoxyModelClass(layout = R.layout.item_timeline_event_image_message)
abstract class MessageImageItem : AbsMessageItem<MessageImageItem.Holder>() { abstract class MessageImageItem : AbsMessageItem<MessageImageItem.Holder>() {
@EpoxyAttribute lateinit var mediaData: MediaContentRenderer.Data @EpoxyAttribute lateinit var mediaData: MediaContentRenderer.Data
@EpoxyAttribute lateinit var eventId: String
@EpoxyAttribute override lateinit var informationData: MessageInformationData @EpoxyAttribute override lateinit var informationData: MessageInformationData
@EpoxyAttribute var clickListener: View.OnClickListener? = null @EpoxyAttribute var clickListener: View.OnClickListener? = null
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
MediaContentRenderer.render(mediaData, MediaContentRenderer.Mode.THUMBNAIL, holder.imageView) MediaContentRenderer.render(mediaData, MediaContentRenderer.Mode.THUMBNAIL, holder.imageView)
ContentUploadStateTrackerBinder.bind(eventId, holder.progressLayout)
holder.imageView.setOnClickListener(clickListener) holder.imageView.setOnClickListener(clickListener)
} }
override fun unbind(holder: Holder) {
ContentUploadStateTrackerBinder.unbind(eventId)
super.unbind(holder)
}
class Holder : AbsMessageItem.Holder() { class Holder : AbsMessageItem.Holder() {
override val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView) override val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
override val memberNameView by bind<TextView>(R.id.messageMemberNameView) override val memberNameView by bind<TextView>(R.id.messageMemberNameView)
override val timeView by bind<TextView>(R.id.messageTimeView) 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) val imageView by bind<ImageView>(R.id.messageImageView)
} }

View file

@ -25,6 +25,7 @@ import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.session.content.ContentUrlResolver import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.riotredesign.core.glide.GlideApp import im.vector.riotredesign.core.glide.GlideApp
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import java.io.File
object MediaContentRenderer { object MediaContentRenderer {
@ -38,7 +39,12 @@ object MediaContentRenderer {
val maxWidth: Int, val maxWidth: Int,
val orientation: Int?, val orientation: Int?,
val rotation: Int? val rotation: Int?
) : Parcelable ) : Parcelable {
fun isLocalFile(): Boolean {
return url != null && File(url).exists()
}
}
enum class Mode { enum class Mode {
FULL_SIZE, FULL_SIZE,
@ -67,13 +73,17 @@ object MediaContentRenderer {
fun render(data: Data, imageView: BigImageView) { fun render(data: Data, imageView: BigImageView) {
val (width, height) = processSize(data, Mode.THUMBNAIL) val (width, height) = processSize(data, Mode.THUMBNAIL)
val contentUrlResolver = Matrix.getInstance().currentSession!!.contentUrlResolver() val contentUrlResolver = Matrix.getInstance().currentSession!!.contentUrlResolver()
if (data.isLocalFile()) {
imageView.showImage(Uri.parse(data.url))
} else {
val fullSize = contentUrlResolver.resolveFullSize(data.url) val fullSize = contentUrlResolver.resolveFullSize(data.url)
val thumbnail = contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE) val thumbnail = contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE)
imageView.showImage( imageView.showImage(
Uri.parse(thumbnail ?: data.url), Uri.parse(thumbnail),
Uri.parse(fullSize ?: data.url) Uri.parse(fullSize)
) )
} }
}
private fun processSize(data: Data, mode: Mode): Pair<Int, Int> { private fun processSize(data: Data, mode: Mode): Pair<Int, Int> {
val maxImageWidth = data.maxWidth val maxImageWidth = data.maxWidth

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 B

View file

@ -59,10 +59,23 @@
android:layout_marginEnd="32dp" android:layout_marginEnd="32dp"
android:layout_marginRight="32dp" android:layout_marginRight="32dp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0" app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/messageMemberNameView" /> 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> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -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>