diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt index ad45328206..b0d5343e4b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt @@ -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. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentUploadStateTracker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentUploadStateTracker.kt new file mode 100644 index 0000000000..0d88e5faf7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentUploadStateTracker.kt @@ -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() + } + + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ProgressRequestBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ProgressRequestBody.kt new file mode 100644 index 0000000000..fabd9763e1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ProgressRequestBody.kt @@ -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) + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt index bace6740ce..a44faf1e7b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt @@ -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() private val syncThread by inject() private val contentUrlResolver by inject() + private val contentUploadProgressTracker by inject() 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) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index 85dcdb0349..ac1faef7c2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -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()) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ContentModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ContentModule.kt new file mode 100644 index 0000000000..b149cd3867 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ContentModule.kt @@ -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() as DefaultContentUploadStateTracker) + } + + scope(DefaultSession.SCOPE) { + val sessionParams = get() + DefaultContentUrlResolver(sessionParams.homeServerConnectionConfig) as ContentUrlResolver + } + + } + + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ContentUploader.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ContentUploader.kt index 4814438c09..42dff4f036 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ContentUploader.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ContentUploader.kt @@ -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 { + fun uploadFile(eventId: String, attachment: ContentAttachmentData): Try { 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 } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUploadStateTracker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUploadStateTracker.kt new file mode 100644 index 0000000000..1e6ca3c1c0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUploadStateTracker.kt @@ -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() + private val listenersByEvent = mutableMapOf>() + + 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) } + } + } + } + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt index c3739864ae..f864e725ff 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt @@ -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() @@ -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(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) }) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt index b1f4277219..6f0fdfe232 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt @@ -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()) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/media/MediaAttachment.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/media/MediaAttachment.kt deleted file mode 100644 index 1f77b451ee..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/media/MediaAttachment.kt +++ /dev/null @@ -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 \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/media/MediaUploader.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/media/MediaUploader.kt deleted file mode 100644 index 7fd9fe9384..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/media/MediaUploader.kt +++ /dev/null @@ -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 { - 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()) - } - } - } -} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/media/UploadMediaWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/media/UploadMediaWorker.kt deleted file mode 100644 index 2e97ef5a21..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/media/UploadMediaWorker.kt +++ /dev/null @@ -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() - - @JsonClass(generateAdapter = true) - internal data class Params( - val attachment: ContentAttachmentData - ) - - override fun doWork(): Result { - val params = WorkerParamsFactory.fromData(inputData) - ?: return Result.failure() - - return mediaUploader - .uploadFile(params.attachment) - .fold({ Result.retry() }, { Result.success() }) - } - - -} diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 8d03e42b34..155745dffa 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -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_() diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt new file mode 100644 index 0000000000..893ab0daa0 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt @@ -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() + + 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(R.id.mediaProgressBar) + val progressTextView = progressLayout.findViewById(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)}" + } + +} diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/AbsMessageItem.kt index 969ba69655..76449364d6 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -28,6 +28,7 @@ abstract class AbsMessageItem : RiotEpoxyModel() { 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 diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageImageItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageImageItem.kt index 8272d6a65f..e990e965ea 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageImageItem.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageImageItem.kt @@ -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() { @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(R.id.messageAvatarImageView) override val memberNameView by bind(R.id.messageMemberNameView) override val timeView by bind(R.id.messageTimeView) + val progressLayout by bind(R.id.messageImageUploadProgressLayout) val imageView by bind(R.id.messageImageView) } diff --git a/vector/src/main/java/im/vector/riotredesign/features/media/MediaContentRenderer.kt b/vector/src/main/java/im/vector/riotredesign/features/media/MediaContentRenderer.kt index 2468bc8019..fda674189d 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/media/MediaContentRenderer.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/media/MediaContentRenderer.kt @@ -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 { diff --git a/vector/src/main/res/drawable-mdpi/vector_cancel_upload_download.png b/vector/src/main/res/drawable-mdpi/vector_cancel_upload_download.png new file mode 100755 index 0000000000..51b4401ca0 Binary files /dev/null and b/vector/src/main/res/drawable-mdpi/vector_cancel_upload_download.png differ diff --git a/vector/src/main/res/layout/item_timeline_event_image_message.xml b/vector/src/main/res/layout/item_timeline_event_image_message.xml index 29d5106ac8..cb3c121a9d 100644 --- a/vector/src/main/res/layout/item_timeline_event_image_message.xml +++ b/vector/src/main/res/layout/item_timeline_event_image_message.xml @@ -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" /> + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/media_upload_download_progress_layout.xml b/vector/src/main/res/layout/media_upload_download_progress_layout.xml new file mode 100644 index 0000000000..a110e08135 --- /dev/null +++ b/vector/src/main/res/layout/media_upload_download_progress_layout.xml @@ -0,0 +1,28 @@ + + + + + + + + +