From b24a37226263ecf10d0450b13814483ab69966eb Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 25 Sep 2019 10:50:13 +0200 Subject: [PATCH 01/14] Show "Clear message queue" option (in debug mode) --- .../home/room/detail/RoomDetailViewModel.kt | 20 ++++++++----------- vector/src/main/res/menu/menu_timeline.xml | 2 +- 2 files changed, 9 insertions(+), 13 deletions(-) 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..ac56114319 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 @@ -48,6 +48,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 @@ -232,18 +233,13 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro 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 ***************************************************************************** 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 @@ From 9b91b6ea87effe986e7539592661b18948514848 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 25 Sep 2019 10:56:18 +0200 Subject: [PATCH 02/14] Create Extension to convert a Response to a Failure --- .../matrix/android/internal/network/Request.kt | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) 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..3420717199 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 @@ -23,9 +23,9 @@ 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 retrofit2.Response import timber.log.Timber import java.io.IOException @@ -43,7 +43,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) { @@ -56,10 +56,8 @@ internal class Request { } } - private fun manageFailure(errorBody: ResponseBody?, httpCode: Int): Throwable { - if (errorBody == null) { - return RuntimeException("Error body should not be null") - } + private fun Response.toFailure(): Failure { + val errorBody = errorBody() ?: return Failure.Unknown(RuntimeException("errorBody() should not be null")) val errorBodyStr = errorBody.string() @@ -74,13 +72,13 @@ internal class Request { EventBus.getDefault().post(ConsentNotGivenError(matrixError.consentUri)) } - return Failure.ServerError(matrixError, httpCode) + return Failure.ServerError(matrixError, code()) } } 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) + return Failure.OtherServerError(errorBodyStr, code()) } } \ No newline at end of file From ae8bceacba1f1b6076e3fc7fb1cd7e0da7ece3b1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 25 Sep 2019 10:58:55 +0200 Subject: [PATCH 03/14] Create Extension to convert a Response to a Failure -> expose to other object --- .../android/internal/network/Request.kt | 35 ------------ .../internal/network/RetrofitExtensions.kt | 54 ++++++++++++++++++- 2 files changed, 52 insertions(+), 37 deletions(-) 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 3420717199..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 org.greenrobot.eventbus.EventBus import retrofit2.Call -import retrofit2.Response -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 { @@ -55,30 +46,4 @@ internal class Request { } } } - - private fun Response.toFailure(): Failure { - val errorBody = errorBody() ?: return Failure.Unknown(RuntimeException("errorBody() 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, code()) - } - } catch (ex: JsonDataException) { - // This is not a MatrixError - Timber.w("The error returned by the server is not a MatrixError") - } - - return Failure.OtherServerError(errorBodyStr, code()) - } } \ 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..64fcb08bac 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,22 @@ 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 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 +48,46 @@ suspend fun Call.awaitResponse(): Response { } }) } -} \ No newline at end of file +} + +/** + * 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) +} From 4c04014e4d998429812289bbc6ecf5f4bf37bc92 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 25 Sep 2019 11:26:49 +0200 Subject: [PATCH 04/14] Do not log big data request (ex: file upload) --- .../interceptors/CurlLoggingInterceptor.kt | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) 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() From f3039601bf135c27b5b74016a3bdd4252a53bb5e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 25 Sep 2019 11:27:23 +0200 Subject: [PATCH 05/14] throw Failure instead of meaning less IOException --- .../matrix/android/internal/session/content/FileUploader.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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..15d75ceeb6 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 @@ -22,6 +22,7 @@ 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.toFailure import okhttp3.* import java.io.File import java.io.IOException @@ -74,7 +75,7 @@ internal class FileUploader @Inject constructor(@Authenticated return Try { okHttpClient.newCall(request).execute().use { response -> if (!response.isSuccessful) { - throw IOException() + throw response.toFailure() } else { response.body()?.source()?.let { responseAdapter.fromJson(it) From f077cc846763689dc3b61331b954589be639928e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 25 Sep 2019 14:09:26 +0200 Subject: [PATCH 06/14] Stop sending media in an infinite loop in case of error (part of #587) Not sure how this commit fix it, but the issue is not observed anymore with it --- .../attachments/MXEncryptedAttachments.kt | 19 +---- .../internal/network/RetrofitExtensions.kt | 19 +++++ .../internal/session/content/FileUploader.kt | 43 +++++----- .../session/content/UploadContentWorker.kt | 79 +++++++++---------- 4 files changed, 77 insertions(+), 83 deletions(-) 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/network/RetrofitExtensions.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt index 64fcb08bac..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 @@ -30,6 +30,7 @@ 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 @@ -50,6 +51,24 @@ internal suspend fun Call.awaitResponse(): Response { } } +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 */ 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 15d75ceeb6..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,11 @@ 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 @@ -38,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) @@ -72,19 +69,15 @@ internal class FileUploader @Inject constructor(@Authenticated .post(requestBody) .build() - return Try { - okHttpClient.newCall(request).execute().use { response -> - if (!response.isSuccessful) { - throw response.toFailure() - } 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 { From 17cba1a43223bebeaae7595509cc958aa5c23f5a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 25 Sep 2019 14:39:33 +0200 Subject: [PATCH 07/14] Display progress in the timeline when uploading file --- .../im/vector/riotx/core/utils/FileUtils.kt | 2 ++ .../timeline/factory/MessageItemFactory.kt | 5 +++++ .../helper/ContentUploadStateTrackerBinder.kt | 9 ++++----- .../detail/timeline/item/MessageFileItem.kt | 18 ++++++++++++++++++ .../timeline/item/MessageImageVideoItem.kt | 7 +++++-- .../features/media/ImageContentRenderer.kt | 6 ++---- .../layout/item_timeline_event_file_stub.xml | 2 +- 7 files changed, 37 insertions(+), 12 deletions(-) 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/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..a7db7c826e 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 @@ -27,7 +27,6 @@ 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.resources.ColorProvider -import im.vector.riotx.features.media.ImageContentRenderer import im.vector.riotx.features.ui.getMessageTextColor import javax.inject.Inject @@ -37,12 +36,12 @@ class ContentUploadStateTrackerBinder @Inject constructor(private val activeSess 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) updateListeners[eventId] = updateListener uploadStateTracker.track(eventId, updateListener) } @@ -60,7 +59,7 @@ class ContentUploadStateTrackerBinder @Inject constructor(private val activeSess } private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup, - private val mediaData: ImageContentRenderer.Data, + private val isLocalFile: Boolean, private val colorProvider: ColorProvider) : ContentUploadStateTracker.UpdateListener { override fun onUpdate(state: ContentUploadStateTracker.State) { @@ -76,7 +75,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) 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 Date: Wed, 25 Sep 2019 14:44:34 +0200 Subject: [PATCH 08/14] Human readable error --- .../helper/ContentUploadStateTrackerBinder.kt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) 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 a7db7c826e..09b2e86a83 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 @@ -26,12 +26,14 @@ 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.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() @@ -41,7 +43,7 @@ class ContentUploadStateTrackerBinder @Inject constructor(private val activeSess activeSessionHolder.getActiveSession().also { session -> val uploadStateTracker = session.contentUploadProgressTracker() - val updateListener = ContentMediaProgressUpdater(progressLayout, isLocalFile, colorProvider) + val updateListener = ContentMediaProgressUpdater(progressLayout, isLocalFile, colorProvider, errorFormatter) updateListeners[eventId] = updateListener uploadStateTracker.track(eventId, updateListener) } @@ -60,7 +62,8 @@ class ContentUploadStateTrackerBinder @Inject constructor(private val activeSess private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup, private val isLocalFile: Boolean, - private val colorProvider: ColorProvider) : ContentUploadStateTracker.UpdateListener { + private val colorProvider: ColorProvider, + private val errorFormatter: ErrorFormatter) : ContentUploadStateTracker.UpdateListener { override fun onUpdate(state: ContentUploadStateTracker.State) { when (state) { @@ -133,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)) } From 643a2baabf082dbcf8301d68b3b5c7a3e1e22407 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 25 Sep 2019 15:03:16 +0200 Subject: [PATCH 09/14] Set click and long click listener even if information data are not displayed --- .../home/room/detail/timeline/item/AbsMessageItem.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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()) { From 1b66d1f746374992f0e30a3226a001f88313c899 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 25 Sep 2019 15:25:26 +0200 Subject: [PATCH 10/14] Fix bad rendering of file item if the filename is long --- .../layout/item_timeline_event_file_stub.xml | 86 ++++++++++--------- 1 file changed, 44 insertions(+), 42 deletions(-) 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 729509b859..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,53 +2,55 @@ + 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" /> - - - - - - - - - + + From a0b1ef3216522f8a468b0e7db47cb20a9ba0e76f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 25 Sep 2019 16:59:50 +0200 Subject: [PATCH 11/14] Do not upload file to big for the homeserver (#587) Also create a HomeServerCapabilitiesService which provide configuration of the homeserver. Data are retrieved every 8 hours (as RiotWeb?) --- CHANGES.md | 3 +- .../matrix/android/api/session/Session.kt | 2 + .../homeserver/HomeServerCapabilities.kt | 28 +++++++ .../HomeServerCapabilitiesService.kt | 29 ++++++++ .../mapper/HomeServerCapabilitiesMapper.kt | 38 ++++++++++ .../model/HomeServerCapabilitiesEntity.kt | 29 ++++++++ .../database/model/SessionRealmModule.kt | 3 +- .../query/HomeServerCapabilitiesQueries.kt | 37 ++++++++++ .../internal/network/NetworkConstants.kt | 5 ++ .../internal/session/DefaultSession.kt | 7 +- .../internal/session/SessionComponent.kt | 2 + .../android/internal/session/SessionModule.kt | 5 ++ .../session/homeserver/CapabilitiesAPI.kt | 31 ++++++++ .../DefaultGetHomeServerCapabilitiesTask.kt | 74 +++++++++++++++++++ .../DefaultHomeServerCapabilitiesService.kt | 37 ++++++++++ .../homeserver/GetUploadCapabilitiesResult.kt | 30 ++++++++ .../HomeServerCapabilitiesModule.kt | 41 ++++++++++ .../android/internal/session/sync/SyncTask.kt | 7 +- .../home/room/detail/FileTooBigError.kt | 23 ++++++ .../home/room/detail/RoomDetailFragment.kt | 17 +++++ .../home/room/detail/RoomDetailViewModel.kt | 20 ++++- vector/src/main/res/values/strings_riotX.xml | 2 + 22 files changed, 463 insertions(+), 7 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/homeserver/HomeServerCapabilities.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/homeserver/HomeServerCapabilitiesService.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/HomeServerCapabilitiesMapper.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/HomeServerCapabilitiesEntity.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/HomeServerCapabilitiesQueries.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/CapabilitiesAPI.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultHomeServerCapabilitiesService.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/GetUploadCapabilitiesResult.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/HomeServerCapabilitiesModule.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/FileTooBigError.kt diff --git a/CHANGES.md b/CHANGES.md index 53ed7748de..396419ea66 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,12 +6,13 @@ Features: Improvements: - Persist active tab between sessions (#503) + - Do not upload file to big for the homeserver (#587) Other changes: - Bugfix: - - + - Fix issue on upload error in loop (#587) Translations: - 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..f7107e9d47 --- /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 sign out. It's implemented at the session level. + */ +interface HomeServerCapabilitiesService { + + /** + * Get the HomeServer capabilities + */ + fun getHomeServerCapabilities(): HomeServerCapabilities + +} 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/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/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..d3cb945adb --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt @@ -0,0 +1,74 @@ +/* + * 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 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.doWithRealm { 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..6f6416ba4a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultHomeServerCapabilitiesService.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.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 entity?.let { HomeServerCapabilitiesMapper.map(it) } ?: HomeServerCapabilities(HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN) + } +} \ No newline at end of file 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/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..7934e0ccae 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 @@ -28,6 +28,7 @@ import android.os.Parcelable import android.text.Editable import android.text.Spannable import android.text.TextUtils +import android.text.format.Formatter import android.view.* import android.view.inputmethod.InputMethodManager import android.widget.TextView @@ -227,6 +228,10 @@ class RoomDetailFragment : scrollOnHighlightedEventCallback.scheduleScrollTo(it) } + roomDetailViewModel.fileTooBigEvent.observeEvent(this) { + displayFileTooBigWarning(it) + } + roomDetailViewModel.selectSubscribe(this, RoomDetailViewState::tombstoneEventHandling, uniqueOnly("tombstoneEventHandling")) { renderTombstoneEventHandling(it) } @@ -254,6 +259,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, + Formatter.formatFileSize(requireContext(), error.homeServerLimitInBytes), + Formatter.formatFileSize(requireContext(), error.fileSizeInBytes) + )) + .setPositiveButton(R.string.ok, null) + .show() + } + private fun setupNotificationView() { notificationAreaView.delegate = object : NotificationAreaView.Delegate { 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 ac56114319..d31a25a8d9 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 @@ -228,6 +229,10 @@ 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 @@ -466,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 { + attachments.find { it.size > maxUploadFileSize } + ?.let { + _fileTooBigEvent.postValue(LiveEvent(FileTooBigError(it.name ?: it.path, it.size, maxUploadFileSize))) + } ?: run { room.sendMedias(attachments) } + } } private fun handleEventDisplayed(action: RoomDetailActions.EventDisplayed) { diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 6b46d359be..7d3312cf24 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 is too large to upload. The file size limit is %2$s but this file is %3$s. + \ No newline at end of file From 60f6b3ef02a6e2b62f031aa6cff161ff50f2c941 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 25 Sep 2019 17:08:58 +0200 Subject: [PATCH 12/14] Auto review --- CHANGES.md | 2 +- .../api/session/homeserver/HomeServerCapabilitiesService.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 396419ea66..dc4c743bc4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,7 +6,7 @@ Features: Improvements: - Persist active tab between sessions (#503) - - Do not upload file to big for the homeserver (#587) + - Do not upload file too big for the homeserver (#587) Other changes: - 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 index f7107e9d47..8e23c21068 100644 --- 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 @@ -17,7 +17,7 @@ package im.vector.matrix.android.api.session.homeserver /** - * This interface defines a method to sign out. It's implemented at the session level. + * This interface defines a method to retrieve the homeserver capabilities. */ interface HomeServerCapabilitiesService { From 62b7a83a31849bb047ec1271f55fee8613c2d169 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 26 Sep 2019 10:08:44 +0200 Subject: [PATCH 13/14] Update after Dominaezzz's review --- .../DefaultGetHomeServerCapabilitiesTask.kt | 3 ++- .../DefaultHomeServerCapabilitiesService.kt | 11 +++++++++-- .../features/home/room/detail/RoomDetailFragment.kt | 4 ++-- .../features/home/room/detail/RoomDetailViewModel.kt | 8 ++++---- vector/src/main/res/values/strings_riotX.xml | 2 +- 5 files changed, 18 insertions(+), 10 deletions(-) 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 index d3cb945adb..defcfbaa81 100644 --- 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 @@ -22,6 +22,7 @@ import im.vector.matrix.android.internal.database.model.HomeServerCapabilitiesEn 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 @@ -35,7 +36,7 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( override suspend fun execute(params: Unit) { var doRequest = false - monarchy.doWithRealm { realm -> + monarchy.awaitTransaction { realm -> val homeServerCapabilitiesEntity = HomeServerCapabilitiesEntity.getOrCreate(realm) doRequest = homeServerCapabilitiesEntity.lastUpdatedTimestamp + MIN_DELAY_BETWEEN_TWO_REQUEST_MILLIS < Date().time 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 index 6f6416ba4a..0af849af2e 100644 --- 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 @@ -32,6 +32,13 @@ internal class DefaultHomeServerCapabilitiesService @Inject constructor(private entity = HomeServerCapabilitiesEntity.getOrCreate(realm) } - return entity?.let { HomeServerCapabilitiesMapper.map(it) } ?: HomeServerCapabilities(HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN) + return with(entity) { + if (this != null) { + HomeServerCapabilitiesMapper.map(this) + } else { + // Should not happen + HomeServerCapabilities(HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN) + } + } } -} \ No newline at end of file +} 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 7934e0ccae..7c7b90cd4b 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 @@ -264,8 +264,8 @@ class RoomDetailFragment : .setTitle(R.string.dialog_title_error) .setMessage(getString(R.string.error_file_too_big, error.filename, - Formatter.formatFileSize(requireContext(), error.homeServerLimitInBytes), - Formatter.formatFileSize(requireContext(), error.fileSizeInBytes) + Formatter.formatFileSize(requireContext(), error.fileSizeInBytes), + Formatter.formatFileSize(requireContext(), error.homeServerLimitInBytes) )) .setPositiveButton(R.string.ok, null) .show() 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 d31a25a8d9..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 @@ -480,10 +480,10 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro // Unknown limitation room.sendMedias(attachments) } else { - attachments.find { it.size > maxUploadFileSize } - ?.let { - _fileTooBigEvent.postValue(LiveEvent(FileTooBigError(it.name ?: it.path, it.size, maxUploadFileSize))) - } ?: run { room.sendMedias(attachments) } + when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) { + null -> room.sendMedias(attachments) + else -> _fileTooBigEvent.postValue(LiveEvent(FileTooBigError(tooBigFile.name ?: tooBigFile.path, tooBigFile.size, maxUploadFileSize))) + } } } diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 7d3312cf24..a48376e085 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -22,6 +22,6 @@ Create a new room Close keys backup banner - The file %1$s is too large to upload. The file size limit is %2$s but this file is %3$s. + "The file '%1$s' (%2$s) is too large to upload. The limit is %3$s." \ No newline at end of file From f02f16d9c55d7bf037abddcbed54ede96ea698c2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 26 Sep 2019 10:41:52 +0200 Subject: [PATCH 14/14] Use IEC units instead of SI units for file sizes --- tools/check/forbidden_strings_in_code.txt | 8 ++++- .../im/vector/riotx/core/utils/TextUtils.kt | 27 +++++++++++++++++ .../home/room/detail/RoomDetailFragment.kt | 29 ++++++++----------- .../helper/ContentUploadStateTrackerBinder.kt | 6 ++-- .../settings/VectorSettingsGeneralFragment.kt | 14 ++++----- 5 files changed, 56 insertions(+), 28 deletions(-) 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/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/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 7c7b90cd4b..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,8 +27,6 @@ import android.os.Bundle import android.os.Parcelable import android.text.Editable import android.text.Spannable -import android.text.TextUtils -import android.text.format.Formatter import android.view.* import android.view.inputmethod.InputMethodManager import android.widget.TextView @@ -149,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() @@ -264,8 +259,8 @@ class RoomDetailFragment : .setTitle(R.string.dialog_title_error) .setMessage(getString(R.string.error_file_too_big, error.filename, - Formatter.formatFileSize(requireContext(), error.fileSizeInBytes), - Formatter.formatFileSize(requireContext(), error.homeServerLimitInBytes) + TextUtils.formatFileSize(requireContext(), error.fileSizeInBytes), + TextUtils.formatFileSize(requireContext(), error.homeServerLimitInBytes) )) .setPositiveButton(R.string.ok, null) .show() @@ -987,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/timeline/helper/ContentUploadStateTrackerBinder.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt index 09b2e86a83..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 @@ -28,6 +27,7 @@ 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.core.utils.TextUtils import im.vector.riotx.features.ui.getMessageTextColor import javax.inject.Inject @@ -126,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)) } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt index 2bec8cf103..e51feb2363 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt @@ -20,7 +20,7 @@ import android.app.Activity import android.content.Context import android.content.Intent import android.text.Editable -import android.text.TextUtils +import android.util.Patterns import android.view.View import android.view.ViewGroup import android.view.inputmethod.InputMethodManager @@ -171,7 +171,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { MXSession.getApplicationSizeCaches(activity, object : SimpleApiCallback() { 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) } }