diff --git a/CHANGES.md b/CHANGES.md index 53ed7748de..dc4c743bc4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,12 +6,13 @@ Features: Improvements: - Persist active tab between sessions (#503) + - Do not upload file too big for the homeserver (#587) Other changes: - Bugfix: - - + - Fix issue on upload error in loop (#587) Translations: - diff --git a/matrix-sdk-android/src/debug/java/im/vector/matrix/android/internal/network/interceptors/CurlLoggingInterceptor.kt b/matrix-sdk-android/src/debug/java/im/vector/matrix/android/internal/network/interceptors/CurlLoggingInterceptor.kt index 3d499be3c1..5863edd154 100644 --- a/matrix-sdk-android/src/debug/java/im/vector/matrix/android/internal/network/interceptors/CurlLoggingInterceptor.kt +++ b/matrix-sdk-android/src/debug/java/im/vector/matrix/android/internal/network/interceptors/CurlLoggingInterceptor.kt @@ -22,6 +22,7 @@ import okhttp3.Interceptor import okhttp3.Response import okhttp3.logging.HttpLoggingInterceptor import okio.Buffer +import timber.log.Timber import java.io.IOException import java.nio.charset.Charset import javax.inject.Inject @@ -58,15 +59,21 @@ internal class CurlLoggingInterceptor @Inject constructor(private val logger: Ht val requestBody = request.body() if (requestBody != null) { - val buffer = Buffer() - requestBody.writeTo(buffer) - var charset: Charset? = UTF8 - val contentType = requestBody.contentType() - if (contentType != null) { - charset = contentType.charset(UTF8) + if (requestBody.contentLength() > 100_000) { + Timber.w("Unable to log curl command data, size is too big (${requestBody.contentLength()})") + // Ensure the curl command will failed + curlCmd += "DATA IS TOO BIG" + } else { + val buffer = Buffer() + requestBody.writeTo(buffer) + var charset: Charset? = UTF8 + val contentType = requestBody.contentType() + if (contentType != null) { + charset = contentType.charset(UTF8) + } + // try to keep to a single line and use a subshell to preserve any line breaks + curlCmd += " --data $'" + buffer.readString(charset!!).replace("\n", "\\n") + "'" } - // try to keep to a single line and use a subshell to preserve any line breaks - curlCmd += " --data $'" + buffer.readString(charset!!).replace("\n", "\\n") + "'" } val headers = request.headers() 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 53dc8e77a0..31e96bb3b8 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 @@ -26,6 +26,7 @@ import im.vector.matrix.android.api.session.content.ContentUrlResolver import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.api.session.group.GroupService +import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService import im.vector.matrix.android.api.session.pushers.PushersService import im.vector.matrix.android.api.session.room.RoomDirectoryService import im.vector.matrix.android.api.session.room.RoomService @@ -52,6 +53,7 @@ interface Session : PushRuleService, PushersService, InitialSyncProgressService, + HomeServerCapabilitiesService, SecureStorageService { /** diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/homeserver/HomeServerCapabilities.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/homeserver/HomeServerCapabilities.kt new file mode 100644 index 0000000000..215516a6c1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/homeserver/HomeServerCapabilities.kt @@ -0,0 +1,28 @@ +/* + * 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.homeserver + +data class HomeServerCapabilities( + /** + * Max size of file which can be uploaded to the homeserver in bytes. [MAX_UPLOAD_FILE_SIZE_UNKNOWN] if unknown or not retrieved yet + */ + val maxUploadFileSize: Long +) { + companion object { + const val MAX_UPLOAD_FILE_SIZE_UNKNOWN = -1L + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/homeserver/HomeServerCapabilitiesService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/homeserver/HomeServerCapabilitiesService.kt new file mode 100644 index 0000000000..8e23c21068 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/homeserver/HomeServerCapabilitiesService.kt @@ -0,0 +1,29 @@ +/* + * 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.homeserver + +/** + * This interface defines a method to retrieve the homeserver capabilities. + */ +interface HomeServerCapabilitiesService { + + /** + * Get the HomeServer capabilities + */ + fun getHomeServerCapabilities(): HomeServerCapabilities + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MXEncryptedAttachments.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MXEncryptedAttachments.kt index c699325562..95ff11d595 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MXEncryptedAttachments.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MXEncryptedAttachments.kt @@ -17,7 +17,6 @@ package im.vector.matrix.android.internal.crypto.attachments import android.util.Base64 -import arrow.core.Try import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileKey import timber.log.Timber @@ -50,7 +49,7 @@ object MXEncryptedAttachments { * @param mimetype the mime type * @return the encryption file info */ - fun encryptAttachment(attachmentStream: InputStream, mimetype: String): Try { + fun encryptAttachment(attachmentStream: InputStream, mimetype: String): EncryptionResult { val t0 = System.currentTimeMillis() val secureRandom = SecureRandom() @@ -70,7 +69,7 @@ object MXEncryptedAttachments { val outStream = ByteArrayOutputStream() - try { + outStream.use { val encryptCipher = Cipher.getInstance(CIPHER_ALGORITHM) val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM) val ivParameterSpec = IvParameterSpec(initVectorBytes) @@ -114,19 +113,7 @@ object MXEncryptedAttachments { ) Timber.v("Encrypt in ${System.currentTimeMillis() - t0} ms") - return Try.just(result) - } catch (oom: OutOfMemoryError) { - Timber.e(oom, "## encryptAttachment failed") - return Try.Failure(oom) - } catch (e: Exception) { - Timber.e(e, "## encryptAttachment failed") - return Try.Failure(e) - } finally { - try { - outStream.close() - } catch (e: Exception) { - Timber.e(e, "## encryptAttachment() : fail to close outStream") - } + return result } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/HomeServerCapabilitiesMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/HomeServerCapabilitiesMapper.kt new file mode 100644 index 0000000000..23ab7b64be --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/HomeServerCapabilitiesMapper.kt @@ -0,0 +1,38 @@ +/* + * 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.database.mapper + +import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities +import im.vector.matrix.android.internal.database.model.HomeServerCapabilitiesEntity + +/** + * HomeServerCapabilitiesEntity <-> HomeSeverCapabilities + */ +internal object HomeServerCapabilitiesMapper { + + fun map(entity: HomeServerCapabilitiesEntity): HomeServerCapabilities { + return HomeServerCapabilities( + entity.maxUploadFileSize + ) + } + + fun map(domain: HomeServerCapabilities): HomeServerCapabilitiesEntity { + return HomeServerCapabilitiesEntity( + domain.maxUploadFileSize + ) + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/HomeServerCapabilitiesEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/HomeServerCapabilitiesEntity.kt new file mode 100644 index 0000000000..2bed0305c7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/HomeServerCapabilitiesEntity.kt @@ -0,0 +1,29 @@ +/* + * 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.database.model + +import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities +import io.realm.RealmObject + +internal open class HomeServerCapabilitiesEntity( + var maxUploadFileSize: Long = HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN, + var lastUpdatedTimestamp: Long = 0L +) : RealmObject() { + + companion object + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt index 680e2eac7d..ffe20d9efe 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt @@ -45,6 +45,7 @@ import io.realm.annotations.RealmModule PusherDataEntity::class, ReadReceiptsSummaryEntity::class, UserDraftsEntity::class, - DraftEntity::class + DraftEntity::class, + HomeServerCapabilitiesEntity::class ]) internal class SessionRealmModule diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/HomeServerCapabilitiesQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/HomeServerCapabilitiesQueries.kt new file mode 100644 index 0000000000..64cd6e4770 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/HomeServerCapabilitiesQueries.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.internal.database.query + +import im.vector.matrix.android.internal.database.model.HomeServerCapabilitiesEntity +import io.realm.Realm +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +/** + * Get the current HomeServerCapabilitiesEntity, create one if it does not exist + */ +internal fun HomeServerCapabilitiesEntity.Companion.getOrCreate(realm: Realm): HomeServerCapabilitiesEntity { + var homeServerCapabilitiesEntity = realm.where().findFirst() + if (homeServerCapabilitiesEntity == null) { + realm.executeTransaction { + realm.createObject() + } + homeServerCapabilitiesEntity = realm.where().findFirst()!! + } + + return homeServerCapabilitiesEntity +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt index cbd4d0c674..02ac778fcc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt @@ -22,4 +22,9 @@ internal object NetworkConstants { const val URI_API_PREFIX_PATH_R0 = "$URI_API_PREFIX_PATH/r0/" const val URI_API_PREFIX_PATH_UNSTABLE = "$URI_API_PREFIX_PATH/unstable/" + + // Media + private const val URI_API_MEDIA_PREFIX_PATH = "_matrix/media" + const val URI_API_MEDIA_PREFIX_PATH_R0 = "$URI_API_MEDIA_PREFIX_PATH/r0/" + } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt index ede9e823bf..a333a02c67 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt @@ -16,24 +16,15 @@ package im.vector.matrix.android.internal.network -import com.squareup.moshi.JsonDataException -import com.squareup.moshi.Moshi -import im.vector.matrix.android.api.failure.ConsentNotGivenError import im.vector.matrix.android.api.failure.Failure -import im.vector.matrix.android.api.failure.MatrixError -import im.vector.matrix.android.internal.di.MoshiProvider import kotlinx.coroutines.CancellationException -import okhttp3.ResponseBody -import org.greenrobot.eventbus.EventBus import retrofit2.Call -import timber.log.Timber import java.io.IOException internal suspend inline fun executeRequest(block: Request.() -> Unit) = Request().apply(block).execute() internal class Request { - private val moshi: Moshi = MoshiProvider.providesMoshi() lateinit var apiCall: Call suspend fun execute(): DATA { @@ -43,7 +34,7 @@ internal class Request { response.body() ?: throw IllegalStateException("The request returned a null body") } else { - throw manageFailure(response.errorBody(), response.code()) + throw response.toFailure() } } catch (exception: Throwable) { throw when (exception) { @@ -55,32 +46,4 @@ internal class Request { } } } - - private fun manageFailure(errorBody: ResponseBody?, httpCode: Int): Throwable { - if (errorBody == null) { - return RuntimeException("Error body should not be null") - } - - val errorBodyStr = errorBody.string() - - val matrixErrorAdapter = moshi.adapter(MatrixError::class.java) - - try { - val matrixError = matrixErrorAdapter.fromJson(errorBodyStr) - - if (matrixError != null) { - if (matrixError.code == MatrixError.M_CONSENT_NOT_GIVEN && !matrixError.consentUri.isNullOrBlank()) { - // Also send this error to the bus, for a global management - EventBus.getDefault().post(ConsentNotGivenError(matrixError.consentUri)) - } - - return Failure.ServerError(matrixError, httpCode) - } - } catch (ex: JsonDataException) { - // This is not a MatrixError - Timber.w("The error returned by the server is not a MatrixError") - } - - return Failure.OtherServerError(errorBodyStr, httpCode) - } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt index 824d74b30e..2bdcd9a2fb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt @@ -18,14 +18,23 @@ package im.vector.matrix.android.internal.network +import com.squareup.moshi.JsonDataException +import im.vector.matrix.android.api.failure.ConsentNotGivenError +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.api.failure.MatrixError +import im.vector.matrix.android.internal.di.MoshiProvider import kotlinx.coroutines.suspendCancellableCoroutine +import okhttp3.ResponseBody +import org.greenrobot.eventbus.EventBus import retrofit2.Call import retrofit2.Callback import retrofit2.Response +import timber.log.Timber +import java.io.IOException import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException -suspend fun Call.awaitResponse(): Response { +internal suspend fun Call.awaitResponse(): Response { return suspendCancellableCoroutine { continuation -> continuation.invokeOnCancellation { cancel() @@ -40,4 +49,64 @@ suspend fun Call.awaitResponse(): Response { } }) } -} \ No newline at end of file +} + +internal suspend fun okhttp3.Call.awaitResponse(): okhttp3.Response { + return suspendCancellableCoroutine { continuation -> + continuation.invokeOnCancellation { + cancel() + } + + enqueue(object : okhttp3.Callback { + override fun onResponse(call: okhttp3.Call, response: okhttp3.Response) { + continuation.resume(response) + } + + override fun onFailure(call: okhttp3.Call, e: IOException) { + continuation.resumeWithException(e) + } + }) + } +} + +/** + * Convert a retrofit Response to a Failure, and eventually parse errorBody to convert it to a MatrixError + */ +internal fun Response.toFailure(): Failure { + return toFailure(errorBody(), code()) +} + +/** + * Convert a okhttp3 Response to a Failure, and eventually parse errorBody to convert it to a MatrixError + */ +internal fun okhttp3.Response.toFailure(): Failure { + return toFailure(body(), code()) +} + +private fun toFailure(errorBody: ResponseBody?, httpCode: Int): Failure { + if (errorBody == null) { + return Failure.Unknown(RuntimeException("errorBody should not be null")) + } + + val errorBodyStr = errorBody.string() + + val matrixErrorAdapter = MoshiProvider.providesMoshi().adapter(MatrixError::class.java) + + try { + val matrixError = matrixErrorAdapter.fromJson(errorBodyStr) + + if (matrixError != null) { + if (matrixError.code == MatrixError.M_CONSENT_NOT_GIVEN && !matrixError.consentUri.isNullOrBlank()) { + // Also send this error to the bus, for a global management + EventBus.getDefault().post(ConsentNotGivenError(matrixError.consentUri)) + } + + return Failure.ServerError(matrixError, httpCode) + } + } catch (ex: JsonDataException) { + // This is not a MatrixError + Timber.w("The error returned by the server is not a MatrixError") + } + + return Failure.OtherServerError(errorBodyStr, httpCode) +} 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 02addaceab..319cce491b 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 @@ -32,6 +32,7 @@ import im.vector.matrix.android.api.session.content.ContentUrlResolver import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.api.session.group.GroupService +import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService import im.vector.matrix.android.api.session.pushers.PushersService import im.vector.matrix.android.api.session.room.RoomDirectoryService import im.vector.matrix.android.api.session.room.RoomService @@ -68,7 +69,8 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se private val syncThreadProvider: Provider, private val contentUrlResolver: ContentUrlResolver, private val contentUploadProgressTracker: ContentUploadStateTracker, - private val initialSyncProgressService: Lazy) + private val initialSyncProgressService: Lazy, + private val homeServerCapabilitiesService: Lazy) : Session, RoomService by roomService.get(), RoomDirectoryService by roomDirectoryService.get(), @@ -81,7 +83,8 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se PushersService by pushersService.get(), FileService by fileService.get(), InitialSyncProgressService by initialSyncProgressService.get(), - SecureStorageService by secureStorageService.get() { + SecureStorageService by secureStorageService.get(), + HomeServerCapabilitiesService by homeServerCapabilitiesService.get() { private var isOpen = false diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt index c8745fc356..b2ed02ff3e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt @@ -30,6 +30,7 @@ import im.vector.matrix.android.internal.session.content.UploadContentWorker import im.vector.matrix.android.internal.session.filter.FilterModule import im.vector.matrix.android.internal.session.group.GetGroupDataWorker import im.vector.matrix.android.internal.session.group.GroupModule +import im.vector.matrix.android.internal.session.homeserver.HomeServerCapabilitiesModule import im.vector.matrix.android.internal.session.pushers.AddHttpPusherWorker import im.vector.matrix.android.internal.session.pushers.PushersModule import im.vector.matrix.android.internal.session.room.RoomModule @@ -51,6 +52,7 @@ import im.vector.matrix.android.internal.task.TaskExecutor SessionModule::class, RoomModule::class, SyncModule::class, + HomeServerCapabilitiesModule::class, SignOutModule::class, GroupModule::class, UserModule::class, 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 db4997ca89..7b655dd939 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 @@ -27,6 +27,7 @@ import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.session.InitialSyncProgressService import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService import im.vector.matrix.android.api.session.securestorage.SecureStorageService import im.vector.matrix.android.internal.database.LiveEntityObserver import im.vector.matrix.android.internal.database.RealmKeysUtils @@ -36,6 +37,7 @@ import im.vector.matrix.android.internal.network.AccessTokenInterceptor import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater +import im.vector.matrix.android.internal.session.homeserver.DefaultHomeServerCapabilitiesService import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater import im.vector.matrix.android.internal.session.room.create.RoomCreateEventLiveObserver import im.vector.matrix.android.internal.session.room.prune.EventsPruner @@ -178,4 +180,7 @@ internal abstract class SessionModule { @Binds abstract fun bindSecureStorageService(secureStorageService: DefaultSecureStorageService): SecureStorageService + @Binds + abstract fun bindHomeServerCapabilitiesService(homeServerCapabilitiesService: DefaultHomeServerCapabilitiesService): HomeServerCapabilitiesService + } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt index 2ec17248d1..2f99199736 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt @@ -16,12 +16,12 @@ package im.vector.matrix.android.internal.session.content -import arrow.core.Try -import arrow.core.Try.Companion.raise import com.squareup.moshi.Moshi import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.internal.di.Authenticated import im.vector.matrix.android.internal.network.ProgressRequestBody +import im.vector.matrix.android.internal.network.awaitResponse +import im.vector.matrix.android.internal.network.toFailure import okhttp3.* import java.io.File import java.io.IOException @@ -37,28 +37,26 @@ internal class FileUploader @Inject constructor(@Authenticated private val responseAdapter = moshi.adapter(ContentUploadResponse::class.java) - fun uploadFile(file: File, - filename: String?, - mimeType: String, - progressListener: ProgressRequestBody.Listener? = null): Try { - + suspend fun uploadFile(file: File, + filename: String?, + mimeType: String, + progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse { val uploadBody = RequestBody.create(MediaType.parse(mimeType), file) return upload(uploadBody, filename, progressListener) } - fun uploadByteArray(byteArray: ByteArray, - filename: String?, - mimeType: String, - progressListener: ProgressRequestBody.Listener? = null): Try { - + suspend fun uploadByteArray(byteArray: ByteArray, + filename: String?, + mimeType: String, + progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse { val uploadBody = RequestBody.create(MediaType.parse(mimeType), byteArray) return upload(uploadBody, filename, progressListener) } - private fun upload(uploadBody: RequestBody, filename: String?, progressListener: ProgressRequestBody.Listener?): Try { - val urlBuilder = HttpUrl.parse(uploadUrl)?.newBuilder() ?: return raise(RuntimeException()) + private suspend fun upload(uploadBody: RequestBody, filename: String?, progressListener: ProgressRequestBody.Listener?): ContentUploadResponse { + val urlBuilder = HttpUrl.parse(uploadUrl)?.newBuilder() ?: throw RuntimeException() val httpUrl = urlBuilder .addQueryParameter("filename", filename) @@ -71,19 +69,15 @@ internal class FileUploader @Inject constructor(@Authenticated .post(requestBody) .build() - return Try { - okHttpClient.newCall(request).execute().use { response -> - if (!response.isSuccessful) { - throw IOException() - } else { - response.body()?.source()?.let { - responseAdapter.fromJson(it) - } - ?: throw IOException() + return okHttpClient.newCall(request).awaitResponse().use { response -> + if (!response.isSuccessful) { + throw response.toFailure() + } else { + response.body()?.source()?.let { + responseAdapter.fromJson(it) } + ?: throw IOException() } } - } - } \ No newline at end of file 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 b015670daa..2d9509b43d 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 @@ -93,32 +93,28 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : } } - val contentUploadResponse = if (params.isRoomEncrypted) { - Timber.v("Encrypt thumbnail") - contentUploadStateTracker.setEncryptingThumbnail(eventId) - MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType) - .flatMap { encryptionResult -> - uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo + try { + val contentUploadResponse = if (params.isRoomEncrypted) { + Timber.v("Encrypt thumbnail") + contentUploadStateTracker.setEncryptingThumbnail(eventId) + val encryptionResult = MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType) + uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo + fileUploader.uploadByteArray(encryptionResult.encryptedByteArray, + "thumb_${attachment.name}", + "application/octet-stream", + thumbnailProgressListener) + } else { + fileUploader.uploadByteArray(thumbnailData.bytes, + "thumb_${attachment.name}", + thumbnailData.mimeType, + thumbnailProgressListener) + } - fileUploader - .uploadByteArray(encryptionResult.encryptedByteArray, - "thumb_${attachment.name}", - "application/octet-stream", - thumbnailProgressListener) - } - } else { - fileUploader - .uploadByteArray(thumbnailData.bytes, - "thumb_${attachment.name}", - thumbnailData.mimeType, - thumbnailProgressListener) + uploadedThumbnailUrl = contentUploadResponse.contentUri + } catch (t: Throwable) { + Timber.e(t) + return handleFailure(params, t) } - - contentUploadResponse - .fold( - { Timber.e(it) }, - { uploadedThumbnailUrl = it.contentUri } - ) } val progressListener = object : ProgressRequestBody.Listener { @@ -133,27 +129,26 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null - val contentUploadResponse = if (params.isRoomEncrypted) { - Timber.v("Encrypt file") - contentUploadStateTracker.setEncrypting(eventId) + return try { + val contentUploadResponse = if (params.isRoomEncrypted) { + Timber.v("Encrypt file") + contentUploadStateTracker.setEncrypting(eventId) - MXEncryptedAttachments.encryptAttachment(FileInputStream(attachmentFile), attachment.mimeType) - .flatMap { encryptionResult -> - uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo + val encryptionResult = MXEncryptedAttachments.encryptAttachment(FileInputStream(attachmentFile), attachment.mimeType) + uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo - fileUploader - .uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener) - } - } else { - fileUploader - .uploadFile(attachmentFile, attachment.name, attachment.mimeType, progressListener) + fileUploader + .uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener) + } else { + fileUploader + .uploadFile(attachmentFile, attachment.name, attachment.mimeType, progressListener) + } + + handleSuccess(params, contentUploadResponse.contentUri, uploadedFileEncryptedFileInfo, uploadedThumbnailUrl, uploadedThumbnailEncryptedFileInfo) + } catch (t: Throwable) { + Timber.e(t) + handleFailure(params, t) } - - return contentUploadResponse - .fold( - { handleFailure(params, it) }, - { handleSuccess(params, it.contentUri, uploadedFileEncryptedFileInfo, uploadedThumbnailUrl, uploadedThumbnailEncryptedFileInfo) } - ) } private fun handleFailure(params: Params, failure: Throwable): Result { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/CapabilitiesAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/CapabilitiesAPI.kt new file mode 100644 index 0000000000..69972a1f57 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/CapabilitiesAPI.kt @@ -0,0 +1,31 @@ +/* + * 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.homeserver + +import im.vector.matrix.android.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.GET + +internal interface CapabilitiesAPI { + + /** + * Request the upload capabilities + */ + @GET(NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "config") + fun getUploadCapabilities(): Call + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt new file mode 100644 index 0000000000..defcfbaa81 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt @@ -0,0 +1,75 @@ +/* + * 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.homeserver + +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities +import im.vector.matrix.android.internal.database.model.HomeServerCapabilitiesEntity +import im.vector.matrix.android.internal.database.query.getOrCreate +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task +import im.vector.matrix.android.internal.util.awaitTransaction +import java.util.* +import javax.inject.Inject + +internal interface GetHomeServerCapabilitiesTask : Task + +internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( + private val capabilitiesAPI: CapabilitiesAPI, + private val monarchy: Monarchy +) : GetHomeServerCapabilitiesTask { + + + override suspend fun execute(params: Unit) { + var doRequest = false + monarchy.awaitTransaction { realm -> + val homeServerCapabilitiesEntity = HomeServerCapabilitiesEntity.getOrCreate(realm) + + doRequest = homeServerCapabilitiesEntity.lastUpdatedTimestamp + MIN_DELAY_BETWEEN_TWO_REQUEST_MILLIS < Date().time + } + + if (!doRequest) { + return + } + + val uploadCapabilities = executeRequest { + apiCall = capabilitiesAPI.getUploadCapabilities() + } + + // TODO Add other call here (get version, etc.) + + insertInDb(uploadCapabilities) + } + + + private fun insertInDb(getUploadCapabilitiesResult: GetUploadCapabilitiesResult) { + monarchy + .writeAsync { realm -> + val homeServerCapabilitiesEntity = HomeServerCapabilitiesEntity.getOrCreate(realm) + + homeServerCapabilitiesEntity.maxUploadFileSize = getUploadCapabilitiesResult.maxUploadSize + ?: HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN + + homeServerCapabilitiesEntity.lastUpdatedTimestamp = Date().time + } + } + + companion object { + // 8 hours like on Riot Web + private const val MIN_DELAY_BETWEEN_TWO_REQUEST_MILLIS = 8 * 60 * 60 * 1000 + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultHomeServerCapabilitiesService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultHomeServerCapabilitiesService.kt new file mode 100644 index 0000000000..0af849af2e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultHomeServerCapabilitiesService.kt @@ -0,0 +1,44 @@ +/* + * 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.homeserver + +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities +import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService +import im.vector.matrix.android.internal.database.mapper.HomeServerCapabilitiesMapper +import im.vector.matrix.android.internal.database.model.HomeServerCapabilitiesEntity +import im.vector.matrix.android.internal.database.query.getOrCreate +import javax.inject.Inject + +internal class DefaultHomeServerCapabilitiesService @Inject constructor(private val monarchy: Monarchy) : HomeServerCapabilitiesService { + + override fun getHomeServerCapabilities(): HomeServerCapabilities { + var entity: HomeServerCapabilitiesEntity? = null + monarchy.doWithRealm { realm -> + entity = HomeServerCapabilitiesEntity.getOrCreate(realm) + } + + return with(entity) { + if (this != null) { + HomeServerCapabilitiesMapper.map(this) + } else { + // Should not happen + HomeServerCapabilities(HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/GetUploadCapabilitiesResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/GetUploadCapabilitiesResult.kt new file mode 100644 index 0000000000..8e410cc834 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/GetUploadCapabilitiesResult.kt @@ -0,0 +1,30 @@ +/* + * 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.homeserver + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class GetUploadCapabilitiesResult( + /** + * The maximum size an upload can be in bytes. Clients SHOULD use this as a guide when uploading content. + * If not listed or null, the size limit should be treated as unknown. + */ + @Json(name = "m.upload.size") + val maxUploadSize: Long? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/HomeServerCapabilitiesModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/HomeServerCapabilitiesModule.kt new file mode 100644 index 0000000000..71b3ee63b8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/HomeServerCapabilitiesModule.kt @@ -0,0 +1,41 @@ +/* + * 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.homeserver + +import dagger.Binds +import dagger.Module +import dagger.Provides +import im.vector.matrix.android.internal.session.SessionScope +import retrofit2.Retrofit + +@Module +internal abstract class HomeServerCapabilitiesModule { + + @Module + companion object { + @Provides + @JvmStatic + @SessionScope + fun providesCapabilitiesAPI(retrofit: Retrofit): CapabilitiesAPI { + return retrofit.create(CapabilitiesAPI::class.java) + } + } + + @Binds + abstract fun bindGetHomeServerCapabilitiesTask(getHomeServerCapabilitiesTask: DefaultGetHomeServerCapabilitiesTask): GetHomeServerCapabilitiesTask + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt index 28d4d5fc48..f9cbd05d26 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.internal.session.sync -import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.R import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.MatrixError @@ -25,6 +24,7 @@ import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService import im.vector.matrix.android.internal.session.filter.FilterRepository +import im.vector.matrix.android.internal.session.homeserver.GetHomeServerCapabilitiesTask import im.vector.matrix.android.internal.session.sync.model.SyncResponse import im.vector.matrix.android.internal.task.Task import javax.inject.Inject @@ -42,11 +42,14 @@ internal class DefaultSyncTask @Inject constructor(private val syncAPI: SyncAPI, private val sessionParamsStore: SessionParamsStore, private val initialSyncProgressService: DefaultInitialSyncProgressService, private val syncTokenStore: SyncTokenStore, - private val monarchy: Monarchy + private val getHomeServerCapabilitiesTask: GetHomeServerCapabilitiesTask ) : SyncTask { override suspend fun execute(params: SyncTask.Params) { + // Maybe refresh the home server capabilities data we know + getHomeServerCapabilitiesTask.execute(Unit) + val requestParams = HashMap() var timeout = 0L val token = syncTokenStore.getLastToken() diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt index 7686cb0b7c..7e00295a48 100644 --- a/tools/check/forbidden_strings_in_code.txt +++ b/tools/check/forbidden_strings_in_code.txt @@ -149,4 +149,10 @@ android\.app\.AlertDialog new Gson\(\) ### Use matrixOneTimeWorkRequestBuilder -import androidx.work.OneTimeWorkRequestBuilder===1 \ No newline at end of file +import androidx.work.OneTimeWorkRequestBuilder===1 + +### Use TextUtils.formatFileSize +Formatter\.formatFileSize===1 + +### Use TextUtils.formatFileSize with short format param to true +Formatter\.formatShortFileSize===1 diff --git a/vector/src/main/java/im/vector/riotx/core/utils/FileUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/FileUtils.kt index 4b2d0682d2..5812c395e5 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/FileUtils.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/FileUtils.kt @@ -24,6 +24,8 @@ import java.io.File // Implementation should return true in case of success typealias ActionOnFile = (file: File) -> Boolean +internal fun String?.isLocalFile() = this != null && File(this).exists() + /* ========================================================================================== * Delete * ========================================================================================== */ diff --git a/vector/src/main/java/im/vector/riotx/core/utils/TextUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/TextUtils.kt index 9d980e7f98..a7fa2cb350 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/TextUtils.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/TextUtils.kt @@ -16,6 +16,9 @@ package im.vector.riotx.core.utils +import android.content.Context +import android.os.Build +import android.text.format.Formatter import java.util.* object TextUtils { @@ -42,4 +45,28 @@ object TextUtils { return value.toString() } } + + /** + * Since Android O, the system considers that 1ko = 1000 bytes instead of 1024 bytes. We want to avoid that for the moment. + */ + fun formatFileSize(context: Context, sizeBytes: Long, useShortFormat: Boolean = false): String { + val normalizedSize = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) { + sizeBytes + } else { + // First convert the size + when { + sizeBytes < 1024 -> sizeBytes + sizeBytes < 1024 * 1024 -> sizeBytes * 1000 / 1024 + sizeBytes < 1024 * 1024 * 1024 -> sizeBytes * 1000 / 1024 * 1000 / 1024 + else -> sizeBytes * 1000 / 1024 * 1000 / 1024 * 1000 / 1024 + } + } + + return if (useShortFormat) { + Formatter.formatShortFileSize(context, normalizedSize) + } else { + Formatter.formatFileSize(context, normalizedSize) + + } + } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/FileTooBigError.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/FileTooBigError.kt new file mode 100644 index 0000000000..0f9bfebb47 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/FileTooBigError.kt @@ -0,0 +1,23 @@ +/* + * 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.riotx.features.home.room.detail + +data class FileTooBigError( + val filename: String, + val fileSizeInBytes: Long, + val homeServerLimitInBytes: Long +) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 7bc5cf7016..ad9201c628 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -27,7 +27,6 @@ import android.os.Bundle import android.os.Parcelable import android.text.Editable import android.text.Spannable -import android.text.TextUtils import android.view.* import android.view.inputmethod.InputMethodManager import android.widget.TextView @@ -148,18 +147,15 @@ class RoomDetailFragment : * @param displayName the display name to sanitize * @return the sanitized display name */ - fun sanitizeDisplayname(displayName: String): String? { - // sanity checks - if (!TextUtils.isEmpty(displayName)) { - val ircPattern = " (IRC)" - - if (displayName.endsWith(ircPattern)) { - return displayName.substring(0, displayName.length - ircPattern.length) - } + private fun sanitizeDisplayName(displayName: String): String { + if (displayName.endsWith(ircPattern)) { + return displayName.substring(0, displayName.length - ircPattern.length) } return displayName } + + private const val ircPattern = " (IRC)" } private val roomDetailArgs: RoomDetailArgs by args() @@ -227,6 +223,10 @@ class RoomDetailFragment : scrollOnHighlightedEventCallback.scheduleScrollTo(it) } + roomDetailViewModel.fileTooBigEvent.observeEvent(this) { + displayFileTooBigWarning(it) + } + roomDetailViewModel.selectSubscribe(this, RoomDetailViewState::tombstoneEventHandling, uniqueOnly("tombstoneEventHandling")) { renderTombstoneEventHandling(it) } @@ -254,6 +254,18 @@ class RoomDetailFragment : } } + private fun displayFileTooBigWarning(error: FileTooBigError) { + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.dialog_title_error) + .setMessage(getString(R.string.error_file_too_big, + error.filename, + TextUtils.formatFileSize(requireContext(), error.fileSizeInBytes), + TextUtils.formatFileSize(requireContext(), error.homeServerLimitInBytes) + )) + .setPositiveButton(R.string.ok, null) + .show() + } + private fun setupNotificationView() { notificationAreaView.delegate = object : NotificationAreaView.Delegate { @@ -970,23 +982,23 @@ class RoomDetailFragment : // var vibrate = false val myDisplayName = session.getUser(session.myUserId)?.displayName - if (TextUtils.equals(myDisplayName, text)) { + if (myDisplayName == text) { // current user - if (TextUtils.isEmpty(composerLayout.composerEditText.text)) { + if (composerLayout.composerEditText.text.isBlank()) { composerLayout.composerEditText.append(Command.EMOTE.command + " ") composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length) // vibrate = true } } else { // another user - if (TextUtils.isEmpty(composerLayout.composerEditText.text)) { + if (composerLayout.composerEditText.text.isBlank()) { // Ensure displayName will not be interpreted as a Slash command if (text.startsWith("/")) { composerLayout.composerEditText.append("\\") } - composerLayout.composerEditText.append(sanitizeDisplayname(text)!! + ": ") + composerLayout.composerEditText.append(sanitizeDisplayName(text) + ": ") } else { - composerLayout.composerEditText.text.insert(composerLayout.composerEditText.selectionStart, sanitizeDisplayname(text)!! + " ") + composerLayout.composerEditText.text.insert(composerLayout.composerEditText.selectionStart, sanitizeDisplayName(text) + " ") } // vibrate = true diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 80f333a76e..f1d2431351 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -37,6 +37,7 @@ import im.vector.matrix.android.api.session.events.model.isImageMessage import im.vector.matrix.android.api.session.events.model.isTextMessage import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.file.FileService +import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageType @@ -48,6 +49,7 @@ import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent import im.vector.matrix.rx.rx +import im.vector.riotx.BuildConfig import im.vector.riotx.R import im.vector.riotx.core.extensions.postLiveEvent import im.vector.riotx.core.intent.getFilenameFromUri @@ -227,23 +229,22 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro val navigateToEvent: LiveData> get() = _navigateToEvent + private val _fileTooBigEvent = MutableLiveData>() + val fileTooBigEvent: LiveData> + get() = _fileTooBigEvent + private val _downloadedFileEvent = MutableLiveData>() val downloadedFileEvent: LiveData> get() = _downloadedFileEvent - fun isMenuItemVisible(@IdRes itemId: Int): Boolean { - if (itemId == R.id.clear_message_queue) { - //For now always disable, woker cancellation is not working properly - return false//timeline.pendingEventCount() > 0 - } - if (itemId == R.id.resend_all) { - return timeline.failedToDeliverEventCount() > 0 - } - if (itemId == R.id.clear_all) { - return timeline.failedToDeliverEventCount() > 0 - } - return false + fun isMenuItemVisible(@IdRes itemId: Int) = when (itemId) { + R.id.clear_message_queue -> + /* For now always disable on production, worker cancellation is not working properly */ + timeline.pendingEventCount() > 0 && BuildConfig.DEBUG + R.id.resend_all -> timeline.failedToDeliverEventCount() > 0 + R.id.clear_all -> timeline.failedToDeliverEventCount() > 0 + else -> false } // PRIVATE METHODS ***************************************************************************** @@ -470,7 +471,20 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro type = ContentAttachmentData.Type.values()[it.mediaType] ) } - room.sendMedias(attachments) + + val homeServerCapabilities = session.getHomeServerCapabilities() + + val maxUploadFileSize = homeServerCapabilities.maxUploadFileSize + + if (maxUploadFileSize == HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN) { + // Unknown limitation + room.sendMedias(attachments) + } else { + when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) { + null -> room.sendMedias(attachments) + else -> _fileTooBigEvent.postValue(LiveEvent(FileTooBigError(tooBigFile.name ?: tooBigFile.path, tooBigFile.size, maxUploadFileSize))) + } + } } private fun handleEventDisplayed(action: RoomDetailActions.EventDisplayed) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 4819db4075..0dfa44563c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -42,6 +42,7 @@ import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.utils.DebouncedClickListener import im.vector.riotx.core.utils.DimensionConverter import im.vector.riotx.core.utils.containsOnlyEmojis +import im.vector.riotx.core.utils.isLocalFile import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder @@ -117,6 +118,8 @@ class MessageItemFactory @Inject constructor( .avatarRenderer(avatarRenderer) .colorProvider(colorProvider) .dimensionConverter(dimensionConverter) + .izLocalFile(messageContent.getFileUrl().isLocalFile()) + .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) .informationData(informationData) .highlighted(highlight) .avatarCallback(callback) @@ -147,6 +150,8 @@ class MessageItemFactory @Inject constructor( .avatarRenderer(avatarRenderer) .colorProvider(colorProvider) .dimensionConverter(dimensionConverter) + .izLocalFile(messageContent.getFileUrl().isLocalFile()) + .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) .informationData(informationData) .highlighted(highlight) .avatarCallback(callback) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt index ca79666747..96cb7f0d8e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt @@ -16,7 +16,6 @@ package im.vector.riotx.features.home.room.detail.timeline.helper -import android.text.format.Formatter import android.view.View import android.view.ViewGroup import android.widget.ProgressBar @@ -26,23 +25,25 @@ import im.vector.matrix.android.api.session.content.ContentUploadStateTracker import im.vector.matrix.android.api.session.room.send.SendState import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder +import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.resources.ColorProvider -import im.vector.riotx.features.media.ImageContentRenderer +import im.vector.riotx.core.utils.TextUtils import im.vector.riotx.features.ui.getMessageTextColor import javax.inject.Inject class ContentUploadStateTrackerBinder @Inject constructor(private val activeSessionHolder: ActiveSessionHolder, - private val colorProvider: ColorProvider) { + private val colorProvider: ColorProvider, + private val errorFormatter: ErrorFormatter) { private val updateListeners = mutableMapOf() fun bind(eventId: String, - mediaData: ImageContentRenderer.Data, + isLocalFile: Boolean, progressLayout: ViewGroup) { activeSessionHolder.getActiveSession().also { session -> val uploadStateTracker = session.contentUploadProgressTracker() - val updateListener = ContentMediaProgressUpdater(progressLayout, mediaData, colorProvider) + val updateListener = ContentMediaProgressUpdater(progressLayout, isLocalFile, colorProvider, errorFormatter) updateListeners[eventId] = updateListener uploadStateTracker.track(eventId, updateListener) } @@ -60,8 +61,9 @@ class ContentUploadStateTrackerBinder @Inject constructor(private val activeSess } private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup, - private val mediaData: ImageContentRenderer.Data, - private val colorProvider: ColorProvider) : ContentUploadStateTracker.UpdateListener { + private val isLocalFile: Boolean, + private val colorProvider: ColorProvider, + private val errorFormatter: ErrorFormatter) : ContentUploadStateTracker.UpdateListener { override fun onUpdate(state: ContentUploadStateTracker.State) { when (state) { @@ -76,7 +78,7 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup, } private fun handleIdle(state: ContentUploadStateTracker.State.Idle) { - if (mediaData.isLocalFile()) { + if (isLocalFile) { progressLayout.isVisible = true val progressBar = progressLayout.findViewById(R.id.mediaProgressBar) val progressTextView = progressLayout.findViewById(R.id.mediaProgressTextView) @@ -124,8 +126,8 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup, progressBar?.isIndeterminate = false progressBar?.progress = percent.toInt() progressTextView?.text = progressLayout.context.getString(resId, - Formatter.formatShortFileSize(progressLayout.context, current), - Formatter.formatShortFileSize(progressLayout.context, total)) + TextUtils.formatFileSize(progressLayout.context, current, true), + TextUtils.formatFileSize(progressLayout.context, total, true)) progressTextView?.setTextColor(colorProvider.getMessageTextColor(SendState.SENDING)) } @@ -134,7 +136,7 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup, val progressBar = progressLayout.findViewById(R.id.mediaProgressBar) val progressTextView = progressLayout.findViewById(R.id.mediaProgressTextView) progressBar?.isVisible = false - progressTextView?.text = state.throwable.localizedMessage + progressTextView?.text = errorFormatter.toHumanReadable(state.throwable) progressTextView?.setTextColor(colorProvider.getMessageTextColor(SendState.UNDELIVERED)) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt index 8cc181bd37..fa132be365 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -111,8 +111,6 @@ abstract class AbsMessageItem : BaseEventItem() { holder.timeView.text = informationData.time holder.memberNameView.text = informationData.memberName avatarRenderer.render(informationData.avatarUrl, informationData.senderId, informationData.memberName?.toString(), holder.avatarImageView) - holder.view.setOnClickListener(cellClickListener) - holder.view.setOnLongClickListener(longClickListener) holder.avatarImageView.setOnLongClickListener(longClickListener) holder.memberNameView.setOnLongClickListener(longClickListener) } else { @@ -121,12 +119,13 @@ abstract class AbsMessageItem : BaseEventItem() { holder.avatarImageView.visibility = View.GONE holder.memberNameView.visibility = View.GONE holder.timeView.visibility = View.GONE - holder.view.setOnClickListener(null) - holder.view.setOnLongClickListener(null) holder.avatarImageView.setOnLongClickListener(null) holder.memberNameView.setOnLongClickListener(null) } + holder.view.setOnClickListener(cellClickListener) + holder.view.setOnLongClickListener(longClickListener) + holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer, _readReceiptsClickListener) if (!shouldShowReactionAtBottom() || informationData.orderedReactionList.isNullOrEmpty()) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageFileItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageFileItem.kt index 45e57b59db..56d6a33bc7 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageFileItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageFileItem.kt @@ -22,9 +22,11 @@ import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.annotation.DrawableRes +import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R +import im.vector.riotx.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder @EpoxyModelClass(layout = R.layout.item_timeline_event_base) abstract class MessageFileItem : AbsMessageItem() { @@ -36,19 +38,35 @@ abstract class MessageFileItem : AbsMessageItem() { var iconRes: Int = 0 @EpoxyAttribute var clickListener: View.OnClickListener? = null + @EpoxyAttribute + var izLocalFile = false + @EpoxyAttribute + lateinit var contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder override fun bind(holder: Holder) { super.bind(holder) renderSendState(holder.fileLayout, holder.filenameView) + if (!informationData.sendState.hasFailed()) { + contentUploadStateTrackerBinder.bind(informationData.eventId, izLocalFile, holder.progressLayout) + } else { + holder.progressLayout.isVisible = false + } holder.filenameView.text = filename holder.fileImageView.setImageResource(iconRes) holder.filenameView.setOnClickListener(clickListener) holder.filenameView.paintFlags = (holder.filenameView.paintFlags or Paint.UNDERLINE_TEXT_FLAG) } + override fun unbind(holder: Holder) { + super.unbind(holder) + + contentUploadStateTrackerBinder.unbind(informationData.eventId) + } + override fun getViewType() = STUB_ID class Holder : AbsMessageItem.Holder(STUB_ID) { + val progressLayout by bind(R.id.messageFileUploadProgressLayout) val fileLayout by bind(R.id.messageFileLayout) val fileImageView by bind(R.id.messageFileImageView) val filenameView by bind(R.id.messageFilenameView) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageImageVideoItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageImageVideoItem.kt index 6f713b17fe..50e263267a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageImageVideoItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageImageVideoItem.kt @@ -20,6 +20,7 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageView import androidx.core.view.ViewCompat +import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R @@ -44,11 +45,13 @@ abstract class MessageImageVideoItem : AbsMessageItem() { override fun onSuccess(size: Long) { if (null != activity) { - it.summary = android.text.format.Formatter.formatFileSize(activity, size) + it.summary = TextUtils.formatFileSize(activity, size) } } }) @@ -189,7 +189,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { val size = getSizeOfFiles(requireContext(), File(requireContext().cacheDir, DiskCache.Factory.DEFAULT_DISK_CACHE_DIR)) - it.summary = android.text.format.Formatter.formatFileSize(activity, size.toLong()) + it.summary = TextUtils.formatFileSize(requireContext(), size.toLong()) it.onPreferenceClickListener = Preference.OnPreferenceClickListener { GlobalScope.launch(Dispatchers.Main) { @@ -208,7 +208,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { File(requireContext().cacheDir, DiskCache.Factory.DEFAULT_DISK_CACHE_DIR)) } - it.summary = android.text.format.Formatter.formatFileSize(activity, newSize.toLong()) + it.summary = TextUtils.formatFileSize(requireContext(), newSize.toLong()) hideLoadingView() } @@ -534,7 +534,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { private fun addEmail(email: String) { // check first if the email syntax is valid // if email is null , then also its invalid email - if (TextUtils.isEmpty(email) || !android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches()) { + if (email.isBlank() || !Patterns.EMAIL_ADDRESS.matcher(email).matches()) { activity?.toast(R.string.auth_invalid_email) return } @@ -719,9 +719,9 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { val newPwd = newPasswordText.text.toString().trim() val newConfirmPwd = confirmNewPasswordText.text.toString().trim() - updateButton.isEnabled = oldPwd.isNotEmpty() && newPwd.isNotEmpty() && TextUtils.equals(newPwd, newConfirmPwd) + updateButton.isEnabled = oldPwd.isNotEmpty() && newPwd.isNotEmpty() && newPwd == newConfirmPwd - if (newPwd.isNotEmpty() && newConfirmPwd.isNotEmpty() && !TextUtils.equals(newPwd, newConfirmPwd)) { + if (newPwd.isNotEmpty() && newConfirmPwd.isNotEmpty() && newPwd != newConfirmPwd) { confirmNewPasswordTil.error = getString(R.string.passwords_do_not_match) } } diff --git a/vector/src/main/res/layout/item_timeline_event_file_stub.xml b/vector/src/main/res/layout/item_timeline_event_file_stub.xml index 039d8092ba..259e22b466 100644 --- a/vector/src/main/res/layout/item_timeline_event_file_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_file_stub.xml @@ -2,66 +2,68 @@ + android:layout_height="wrap_content" + android:paddingTop="8dp" + android:paddingBottom="8dp"> - + + + + + + + app:layout_constraintStart_toEndOf="@+id/messageFileImageView" + app:layout_constraintTop_toTopOf="parent" + tools:text="A filename here" /> - - - - - - - - - + + diff --git a/vector/src/main/res/menu/menu_timeline.xml b/vector/src/main/res/menu/menu_timeline.xml index 824735406f..7eea0e2582 100644 --- a/vector/src/main/res/menu/menu_timeline.xml +++ b/vector/src/main/res/menu/menu_timeline.xml @@ -22,7 +22,7 @@ diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 6b46d359be..a48376e085 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -22,4 +22,6 @@ Create a new room Close keys backup banner + "The file '%1$s' (%2$s) is too large to upload. The limit is %3$s." + \ No newline at end of file