Improve file too big error detection and rendering (#3245)

This commit is contained in:
Benoit Marty 2021-04-30 15:34:15 +02:00 committed by Benoit Marty
parent 4a23d31271
commit e108534a2a
18 changed files with 130 additions and 12 deletions

View file

@ -8,6 +8,7 @@ Improvements 🙌:
- Add ability to install APK from directly from Element (#2381)
- Delete and react to stickers (#3250)
- Compress video before sending (#442)
- Improve file too big error detection (#3245)
Bugfix 🐛:
- Message states cosmetic changes (#3007)

View file

@ -28,6 +28,8 @@ import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
import org.matrix.android.sdk.internal.di.MoshiProvider
import org.json.JSONObject
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.failure.MatrixError
import timber.log.Timber
typealias Content = JsonDict
@ -90,6 +92,16 @@ data class Event(
@Transient
var sendState: SendState = SendState.UNKNOWN
@Transient
var sendStateDetails: String? = null
fun sendStateError(): MatrixError? {
return sendStateDetails?.let {
val matrixErrorAdapter = MoshiProvider.providesMoshi().adapter(MatrixError::class.java)
tryOrNull { matrixErrorAdapter.fromJson(it) }
}
}
/**
* The `age` value transcoded in a timestamp based on the device clock when the SDK received
* the event from the home server.

View file

@ -23,6 +23,7 @@ import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.room.RoomAPI
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.toMatrixErrorStr
import javax.inject.Inject
internal interface SendVerificationMessageTask : Task<SendVerificationMessageTask.Params, String> {
@ -55,7 +56,7 @@ internal class DefaultSendVerificationMessageTask @Inject constructor(
localEchoRepository.updateSendState(localId, event.roomId, SendState.SENT)
return response.eventId
} catch (e: Throwable) {
localEchoRepository.updateSendState(localId, event.roomId, SendState.UNDELIVERED)
localEchoRepository.updateSendState(localId, event.roomId, SendState.UNDELIVERED, e.toMatrixErrorStr())
throw e
}
}

View file

@ -43,7 +43,7 @@ import javax.inject.Inject
class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
companion object {
const val SESSION_STORE_SCHEMA_VERSION = 10L
const val SESSION_STORE_SCHEMA_VERSION = 11L
}
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
@ -59,6 +59,7 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
if (oldVersion <= 7) migrateTo8(realm)
if (oldVersion <= 8) migrateTo9(realm)
if (oldVersion <= 9) migrateTo10(realm)
if (oldVersion <= 10) migrateTo11(realm)
}
private fun migrateTo1(realm: DynamicRealm) {
@ -163,7 +164,7 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
?.addRealmListField(EditAggregatedSummaryEntityFields.EDITIONS.`$`, editionOfEventSchema)
}
fun migrateTo9(realm: DynamicRealm) {
private fun migrateTo9(realm: DynamicRealm) {
Timber.d("Step 8 -> 9")
realm.schema.get("RoomSummaryEntity")
@ -201,7 +202,7 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
}
}
fun migrateTo10(realm: DynamicRealm) {
private fun migrateTo10(realm: DynamicRealm) {
Timber.d("Step 9 -> 10")
realm.schema.create("SpaceChildSummaryEntity")
?.addField(SpaceChildSummaryEntityFields.ORDER, String::class.java)
@ -240,4 +241,10 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
?.addRealmListField(RoomSummaryEntityFields.PARENTS.`$`, realm.schema.get("SpaceParentSummaryEntity")!!)
?.addRealmListField(RoomSummaryEntityFields.CHILDREN.`$`, realm.schema.get("SpaceChildSummaryEntity")!!)
}
private fun migrateTo11(realm: DynamicRealm) {
Timber.d("Step 10 -> 11")
realm.schema.get("EventEntity")
?.addField(EventEntityFields.SEND_STATE_DETAILS, String::class.java)
}
}

View file

@ -80,6 +80,7 @@ internal object EventMapper {
).also {
it.ageLocalTs = eventEntity.ageLocalTs
it.sendState = eventEntity.sendState
it.sendStateDetails = eventEntity.sendStateDetails
eventEntity.decryptionResultJson?.let { json ->
try {
it.mxDecryptionResult = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).fromJson(json)

View file

@ -22,6 +22,7 @@ import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
import org.matrix.android.sdk.internal.di.MoshiProvider
import io.realm.RealmObject
import io.realm.annotations.Index
import org.matrix.android.sdk.api.failure.MatrixError
internal open class EventEntity(@Index var eventId: String = "",
@Index var roomId: String = "",
@ -32,6 +33,8 @@ internal open class EventEntity(@Index var eventId: String = "",
@Index var stateKey: String? = null,
var originServerTs: Long? = null,
@Index var sender: String? = null,
// Can contain a serialized MatrixError
var sendStateDetails: String? = null,
var age: Long? = 0,
var unsignedData: String? = null,
var redacts: String? = null,

View file

@ -31,12 +31,16 @@ import okhttp3.RequestBody.Companion.toRequestBody
import okio.BufferedSink
import okio.source
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.MatrixError
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
import org.matrix.android.sdk.internal.di.Authenticated
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.ProgressRequestBody
import org.matrix.android.sdk.internal.network.awaitResponse
import org.matrix.android.sdk.internal.network.toFailure
import org.matrix.android.sdk.internal.session.homeserver.DefaultHomeServerCapabilitiesService
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
@ -46,6 +50,7 @@ import javax.inject.Inject
internal class FileUploader @Inject constructor(@Authenticated
private val okHttpClient: OkHttpClient,
private val globalErrorReceiver: GlobalErrorReceiver,
private val homeServerCapabilitiesService: DefaultHomeServerCapabilitiesService,
private val context: Context,
contentUrlResolver: ContentUrlResolver,
moshi: Moshi) {
@ -57,6 +62,22 @@ internal class FileUploader @Inject constructor(@Authenticated
filename: String?,
mimeType: String?,
progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse {
// Check size limit
// DO NOT COMMIT: 5 Mo
val maxUploadFileSize = 5 * 1024 * 1024L // homeServerCapabilitiesService.getHomeServerCapabilities().maxUploadFileSize
if (maxUploadFileSize != HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN
&& file.length() > maxUploadFileSize) {
// Known limitation and file too big for the server, save the pain to upload it
throw Failure.ServerError(
error = MatrixError(
code = MatrixError.M_TOO_LARGE,
message = "Cannot upload files larger than ${maxUploadFileSize / 1048576L}mb"
),
httpCode = 413
)
}
val uploadBody = object : RequestBody() {
override fun contentLength() = file.length()

View file

@ -41,6 +41,7 @@ import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker
import org.matrix.android.sdk.internal.session.room.send.LocalEchoIdentifiers
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
import org.matrix.android.sdk.internal.session.room.send.MultipleEventSendingDispatcherWorker
import org.matrix.android.sdk.internal.util.toMatrixErrorStr
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
@ -129,6 +130,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
}
}
// TODO Send the Thumbnail after the main content, because the main content can fail if too large.
val uploadThumbnailResult = dealWithThumbnail(params)
val progressListener = object : ProgressRequestBody.Listener {
@ -304,7 +306,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
return Result.success(
WorkerParamsFactory.toData(
params.copy(
lastFailureMessage = failure.localizedMessage
lastFailureMessage = failure.toMatrixErrorStr()
)
)
)

View file

@ -99,6 +99,7 @@ internal class EventEditor @Inject constructor(private val eventSenderProcessor:
entity.age = editedEventEntity.age
entity.originServerTs = editedEventEntity.originServerTs
entity.sendState = editedEventEntity.sendState
entity.sendStateDetails = editedEventEntity.sendStateDetails
}
}
}

View file

@ -87,7 +87,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
}
}
fun updateSendState(eventId: String, roomId: String?, sendState: SendState) {
fun updateSendState(eventId: String, roomId: String?, sendState: SendState, sendStateDetails: String? = null) {
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Update local state of $eventId to ${sendState.name}")
timelineInput.onLocalEchoUpdated(roomId = roomId ?: "", eventId = eventId, sendState = sendState)
updateEchoAsync(eventId) { realm, sendingEventEntity ->
@ -96,6 +96,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
} else {
sendingEventEntity.sendState = sendState
}
sendingEventEntity.sendStateDetails = sendStateDetails
roomSummaryUpdater.updateSendingInformation(realm, sendingEventEntity.roomId)
}
}
@ -161,6 +162,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
val timelineEvents = TimelineEventEntity.where(realm, roomId, eventIds).findAll()
timelineEvents.forEach {
it.root?.sendState = sendState
it.root?.sendStateDetails = null
}
roomSummaryUpdater.updateSendingInformation(realm, roomId)
}

View file

@ -55,7 +55,12 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo
override fun doOnError(params: Params): Result {
params.localEchoIds.forEach { localEchoIds ->
localEchoRepository.updateSendState(localEchoIds.eventId, localEchoIds.roomId, SendState.UNDELIVERED)
localEchoRepository.updateSendState(
eventId = localEchoIds.eventId,
roomId = localEchoIds.roomId,
sendState = SendState.UNDELIVERED,
sendStateDetails = params.lastFailureMessage
)
}
return super.doOnError(params)

View file

@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.crypto.tasks.SendEventTask
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.SessionComponent
import org.matrix.android.sdk.internal.util.toMatrixErrorStr
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import timber.log.Timber
@ -77,7 +78,12 @@ internal class SendEventWorker(context: Context,
}
if (params.lastFailureMessage != null) {
localEchoRepository.updateSendState(event.eventId, event.roomId, SendState.UNDELIVERED)
localEchoRepository.updateSendState(
eventId = event.eventId,
roomId = event.roomId,
sendState = SendState.UNDELIVERED,
sendStateDetails = params.lastFailureMessage
)
// Transmit the error
return Result.success(inputData)
.also { Timber.e("Work cancelled due to input error from parent") }
@ -90,7 +96,12 @@ internal class SendEventWorker(context: Context,
} catch (exception: Throwable) {
if (/*currentAttemptCount >= MAX_NUMBER_OF_RETRY_BEFORE_FAILING ||**/ !exception.shouldBeRetried()) {
Timber.e("## SendEvent: [${System.currentTimeMillis()}] Send event Failed cannot retry ${params.eventId} > ${exception.localizedMessage}")
localEchoRepository.updateSendState(event.eventId, event.roomId, SendState.UNDELIVERED)
localEchoRepository.updateSendState(
eventId = event.eventId,
roomId = event.roomId,
sendState = SendState.UNDELIVERED,
sendStateDetails = exception.toMatrixErrorStr()
)
Result.success()
} else {
Timber.e("## SendEvent: [${System.currentTimeMillis()}] Send event Failed schedule retry ${params.eventId} > ${exception.localizedMessage}")

View file

@ -0,0 +1,36 @@
/*
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.util
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.MatrixError
import org.matrix.android.sdk.internal.di.MoshiProvider
/**
* Try to extract and serialize a MatrixError, or default to localizedMessage
*/
internal fun Throwable.toMatrixErrorStr(): String {
return (this as? Failure.ServerError)
?.let {
// Serialize the MatrixError in this case
val adapter = MoshiProvider.providesMoshi().adapter(MatrixError::class.java)
tryOrNull { adapter.toJson(error) }
}
?: localizedMessage
?: "error"
}

View file

@ -78,6 +78,9 @@ class DefaultErrorFormatter @Inject constructor(
throwable.error.code == MatrixError.M_LIMIT_EXCEEDED -> {
limitExceededError(throwable.error)
}
throwable.error.code == MatrixError.M_TOO_LARGE -> {
stringProvider.getString(R.string.error_file_too_big_simple)
}
throwable.error.code == MatrixError.M_THREEPID_NOT_FOUND -> {
stringProvider.getString(R.string.login_reset_password_error_not_found)
}

View file

@ -54,6 +54,7 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
object ShowWaitingView : RoomDetailViewEvents()
object HideWaitingView : RoomDetailViewEvents()
// TODO Remove
data class FileTooBigError(
val filename: String,
val fileSizeInBytes: Long,

View file

@ -79,7 +79,6 @@ import org.matrix.android.sdk.api.session.events.model.isTextMessage
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.file.FileService
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
import org.matrix.android.sdk.api.session.room.model.Membership
@ -292,7 +291,6 @@ class RoomDetailViewModel @AssistedInject constructor(
is RoomDetailAction.HandleTombstoneEvent -> handleTombstoneEvent(action)
is RoomDetailAction.ResendMessage -> handleResendEvent(action)
is RoomDetailAction.RemoveFailedEcho -> handleRemove(action)
is RoomDetailAction.ResendAll -> handleResendAll()
is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead()
is RoomDetailAction.ReportContent -> handleReportContent(action)
is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action)
@ -1107,6 +1105,10 @@ class RoomDetailViewModel @AssistedInject constructor(
}
private fun handleSendMedia(action: RoomDetailAction.SendMedia) {
room.sendMedias(action.attachments, action.compressBeforeSending, emptySet())
/*
TODO Cleanup this error is now managed by the SDK
val attachments = action.attachments
val homeServerCapabilities = session.getHomeServerCapabilities()
val maxUploadFileSize = homeServerCapabilities.maxUploadFileSize
@ -1124,6 +1126,7 @@ class RoomDetailViewModel @AssistedInject constructor(
))
}
}
*/
}
private fun handleEventVisible(action: RoomDetailAction.TimelineEventTurnsVisible) {

View file

@ -28,6 +28,7 @@ import im.vector.app.core.epoxy.bottomsheet.bottomSheetMessagePreviewItem
import im.vector.app.core.epoxy.bottomsheet.bottomSheetQuickReactionsItem
import im.vector.app.core.epoxy.bottomsheet.bottomSheetSendStateItem
import im.vector.app.core.epoxy.dividerItem
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.DimensionConverter
import im.vector.app.features.home.AvatarRenderer
@ -38,6 +39,7 @@ import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovement
import im.vector.app.features.home.room.detail.timeline.tools.linkify
import im.vector.app.features.media.ImageContentRenderer
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.session.room.send.SendState
import javax.inject.Inject
@ -50,6 +52,7 @@ class MessageActionsEpoxyController @Inject constructor(
private val fontProvider: EmojiCompatFontProvider,
private val imageContentRenderer: ImageContentRenderer,
private val dimensionConverter: DimensionConverter,
private val errorFormatter: ErrorFormatter,
private val dateFormatter: VectorDateFormatter
) : TypedEpoxyController<MessageActionState>() {
@ -74,10 +77,14 @@ class MessageActionsEpoxyController @Inject constructor(
// Send state
val sendState = state.sendState()
if (sendState?.hasFailed().orFalse()) {
// Get more details about the error
val errorMessage = state.timelineEvent()?.root?.sendStateError()
?.let { errorFormatter.toHumanReadable(Failure.ServerError(it, 0)) }
?: stringProvider.getString(R.string.unable_to_send_message)
bottomSheetSendStateItem {
id("send_state")
showProgress(false)
text(stringProvider.getString(R.string.unable_to_send_message))
text(errorMessage)
drawableStart(R.drawable.ic_warning_badge)
}
} else if (sendState?.isSending().orFalse()) {

View file

@ -2295,6 +2295,7 @@
<item quantity="other">%d users read</item>
</plurals>
<string name="error_file_too_big_simple">"The file is too large to upload."</string>
<string name="error_file_too_big">"The file '%1$s' (%2$s) is too large to upload. The limit is %3$s."</string>
<string name="error_attachment">"An error occurred while retrieving the attachment."</string>