Merge pull request #589 from vector-im/feature/media_upload_failure
Fix media upload failure
This commit is contained in:
commit
99de40c980
|
@ -6,12 +6,13 @@ Features:
|
|||
|
||||
Improvements:
|
||||
- Persist active tab between sessions (#503)
|
||||
- Do not upload file too big for the homeserver (#587)
|
||||
|
||||
Other changes:
|
||||
-
|
||||
|
||||
Bugfix:
|
||||
-
|
||||
- Fix issue on upload error in loop (#587)
|
||||
|
||||
Translations:
|
||||
-
|
||||
|
|
|
@ -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,6 +59,11 @@ internal class CurlLoggingInterceptor @Inject constructor(private val logger: Ht
|
|||
|
||||
val requestBody = request.body()
|
||||
if (requestBody != null) {
|
||||
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
|
||||
|
@ -68,6 +74,7 @@ internal class CurlLoggingInterceptor @Inject constructor(private val logger: Ht
|
|||
// 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()
|
||||
var i = 0
|
||||
|
|
|
@ -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 {
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.homeserver
|
||||
|
||||
/**
|
||||
* This interface defines a method to retrieve the homeserver capabilities.
|
||||
*/
|
||||
interface HomeServerCapabilitiesService {
|
||||
|
||||
/**
|
||||
* Get the HomeServer capabilities
|
||||
*/
|
||||
fun getHomeServerCapabilities(): HomeServerCapabilities
|
||||
|
||||
}
|
|
@ -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<EncryptionResult> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
}
|
|
@ -45,6 +45,7 @@ import io.realm.annotations.RealmModule
|
|||
PusherDataEntity::class,
|
||||
ReadReceiptsSummaryEntity::class,
|
||||
UserDraftsEntity::class,
|
||||
DraftEntity::class
|
||||
DraftEntity::class,
|
||||
HomeServerCapabilitiesEntity::class
|
||||
])
|
||||
internal class SessionRealmModule
|
||||
|
|
|
@ -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<HomeServerCapabilitiesEntity>().findFirst()
|
||||
if (homeServerCapabilitiesEntity == null) {
|
||||
realm.executeTransaction {
|
||||
realm.createObject<HomeServerCapabilitiesEntity>()
|
||||
}
|
||||
homeServerCapabilitiesEntity = realm.where<HomeServerCapabilitiesEntity>().findFirst()!!
|
||||
}
|
||||
|
||||
return homeServerCapabilitiesEntity
|
||||
}
|
|
@ -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/"
|
||||
|
||||
}
|
||||
|
|
|
@ -16,24 +16,15 @@
|
|||
|
||||
package im.vector.matrix.android.internal.network
|
||||
|
||||
import com.squareup.moshi.JsonDataException
|
||||
import com.squareup.moshi.Moshi
|
||||
import im.vector.matrix.android.api.failure.ConsentNotGivenError
|
||||
import im.vector.matrix.android.api.failure.Failure
|
||||
import im.vector.matrix.android.api.failure.MatrixError
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import okhttp3.ResponseBody
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import retrofit2.Call
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
|
||||
internal suspend inline fun <DATA> executeRequest(block: Request<DATA>.() -> Unit) = Request<DATA>().apply(block).execute()
|
||||
|
||||
internal class Request<DATA> {
|
||||
|
||||
private val moshi: Moshi = MoshiProvider.providesMoshi()
|
||||
lateinit var apiCall: Call<DATA>
|
||||
|
||||
suspend fun execute(): DATA {
|
||||
|
@ -43,7 +34,7 @@ internal class Request<DATA> {
|
|||
response.body()
|
||||
?: throw IllegalStateException("The request returned a null body")
|
||||
} else {
|
||||
throw manageFailure(response.errorBody(), response.code())
|
||||
throw response.toFailure()
|
||||
}
|
||||
} catch (exception: Throwable) {
|
||||
throw when (exception) {
|
||||
|
@ -55,32 +46,4 @@ internal class Request<DATA> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun manageFailure(errorBody: ResponseBody?, httpCode: Int): Throwable {
|
||||
if (errorBody == null) {
|
||||
return RuntimeException("Error body should not be null")
|
||||
}
|
||||
|
||||
val errorBodyStr = errorBody.string()
|
||||
|
||||
val matrixErrorAdapter = moshi.adapter(MatrixError::class.java)
|
||||
|
||||
try {
|
||||
val matrixError = matrixErrorAdapter.fromJson(errorBodyStr)
|
||||
|
||||
if (matrixError != null) {
|
||||
if (matrixError.code == MatrixError.M_CONSENT_NOT_GIVEN && !matrixError.consentUri.isNullOrBlank()) {
|
||||
// Also send this error to the bus, for a global management
|
||||
EventBus.getDefault().post(ConsentNotGivenError(matrixError.consentUri))
|
||||
}
|
||||
|
||||
return Failure.ServerError(matrixError, httpCode)
|
||||
}
|
||||
} catch (ex: JsonDataException) {
|
||||
// This is not a MatrixError
|
||||
Timber.w("The error returned by the server is not a MatrixError")
|
||||
}
|
||||
|
||||
return Failure.OtherServerError(errorBodyStr, httpCode)
|
||||
}
|
||||
}
|
|
@ -18,14 +18,23 @@
|
|||
|
||||
package im.vector.matrix.android.internal.network
|
||||
|
||||
import com.squareup.moshi.JsonDataException
|
||||
import im.vector.matrix.android.api.failure.ConsentNotGivenError
|
||||
import im.vector.matrix.android.api.failure.Failure
|
||||
import im.vector.matrix.android.api.failure.MatrixError
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import okhttp3.ResponseBody
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
suspend fun <T> Call<T>.awaitResponse(): Response<T> {
|
||||
internal suspend fun <T> Call<T>.awaitResponse(): Response<T> {
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
continuation.invokeOnCancellation {
|
||||
cancel()
|
||||
|
@ -41,3 +50,63 @@ suspend fun <T> Call<T>.awaitResponse(): Response<T> {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun okhttp3.Call.awaitResponse(): okhttp3.Response {
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
continuation.invokeOnCancellation {
|
||||
cancel()
|
||||
}
|
||||
|
||||
enqueue(object : okhttp3.Callback {
|
||||
override fun onResponse(call: okhttp3.Call, response: okhttp3.Response) {
|
||||
continuation.resume(response)
|
||||
}
|
||||
|
||||
override fun onFailure(call: okhttp3.Call, e: IOException) {
|
||||
continuation.resumeWithException(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a retrofit Response to a Failure, and eventually parse errorBody to convert it to a MatrixError
|
||||
*/
|
||||
internal fun <T> Response<T>.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)
|
||||
}
|
||||
|
|
|
@ -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<SyncThread>,
|
||||
private val contentUrlResolver: ContentUrlResolver,
|
||||
private val contentUploadProgressTracker: ContentUploadStateTracker,
|
||||
private val initialSyncProgressService: Lazy<InitialSyncProgressService>)
|
||||
private val initialSyncProgressService: Lazy<InitialSyncProgressService>,
|
||||
private val homeServerCapabilitiesService: Lazy<HomeServerCapabilitiesService>)
|
||||
: 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
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
|
@ -16,12 +16,12 @@
|
|||
|
||||
package im.vector.matrix.android.internal.session.content
|
||||
|
||||
import arrow.core.Try
|
||||
import arrow.core.Try.Companion.raise
|
||||
import com.squareup.moshi.Moshi
|
||||
import im.vector.matrix.android.api.auth.data.SessionParams
|
||||
import im.vector.matrix.android.internal.di.Authenticated
|
||||
import im.vector.matrix.android.internal.network.ProgressRequestBody
|
||||
import im.vector.matrix.android.internal.network.awaitResponse
|
||||
import im.vector.matrix.android.internal.network.toFailure
|
||||
import okhttp3.*
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
@ -37,28 +37,26 @@ internal class FileUploader @Inject constructor(@Authenticated
|
|||
private val responseAdapter = moshi.adapter(ContentUploadResponse::class.java)
|
||||
|
||||
|
||||
fun uploadFile(file: File,
|
||||
suspend fun uploadFile(file: File,
|
||||
filename: String?,
|
||||
mimeType: String,
|
||||
progressListener: ProgressRequestBody.Listener? = null): Try<ContentUploadResponse> {
|
||||
|
||||
progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse {
|
||||
val uploadBody = RequestBody.create(MediaType.parse(mimeType), file)
|
||||
return upload(uploadBody, filename, progressListener)
|
||||
|
||||
}
|
||||
|
||||
fun uploadByteArray(byteArray: ByteArray,
|
||||
suspend fun uploadByteArray(byteArray: ByteArray,
|
||||
filename: String?,
|
||||
mimeType: String,
|
||||
progressListener: ProgressRequestBody.Listener? = null): Try<ContentUploadResponse> {
|
||||
|
||||
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<ContentUploadResponse> {
|
||||
val urlBuilder = HttpUrl.parse(uploadUrl)?.newBuilder() ?: return raise(RuntimeException())
|
||||
private suspend fun upload(uploadBody: RequestBody, filename: String?, progressListener: ProgressRequestBody.Listener?): ContentUploadResponse {
|
||||
val urlBuilder = HttpUrl.parse(uploadUrl)?.newBuilder() ?: throw RuntimeException()
|
||||
|
||||
val httpUrl = urlBuilder
|
||||
.addQueryParameter("filename", filename)
|
||||
|
@ -71,10 +69,9 @@ internal class FileUploader @Inject constructor(@Authenticated
|
|||
.post(requestBody)
|
||||
.build()
|
||||
|
||||
return Try {
|
||||
okHttpClient.newCall(request).execute().use { response ->
|
||||
return okHttpClient.newCall(request).awaitResponse().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
throw IOException()
|
||||
throw response.toFailure()
|
||||
} else {
|
||||
response.body()?.source()?.let {
|
||||
responseAdapter.fromJson(it)
|
||||
|
@ -83,7 +80,4 @@ internal class FileUploader @Inject constructor(@Authenticated
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -93,32 +93,28 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
|
|||
}
|
||||
}
|
||||
|
||||
try {
|
||||
val contentUploadResponse = if (params.isRoomEncrypted) {
|
||||
Timber.v("Encrypt thumbnail")
|
||||
contentUploadStateTracker.setEncryptingThumbnail(eventId)
|
||||
MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType)
|
||||
.flatMap { encryptionResult ->
|
||||
val encryptionResult = MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType)
|
||||
uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo
|
||||
|
||||
fileUploader
|
||||
.uploadByteArray(encryptionResult.encryptedByteArray,
|
||||
fileUploader.uploadByteArray(encryptionResult.encryptedByteArray,
|
||||
"thumb_${attachment.name}",
|
||||
"application/octet-stream",
|
||||
thumbnailProgressListener)
|
||||
}
|
||||
} else {
|
||||
fileUploader
|
||||
.uploadByteArray(thumbnailData.bytes,
|
||||
fileUploader.uploadByteArray(thumbnailData.bytes,
|
||||
"thumb_${attachment.name}",
|
||||
thumbnailData.mimeType,
|
||||
thumbnailProgressListener)
|
||||
}
|
||||
|
||||
contentUploadResponse
|
||||
.fold(
|
||||
{ Timber.e(it) },
|
||||
{ uploadedThumbnailUrl = it.contentUri }
|
||||
)
|
||||
uploadedThumbnailUrl = contentUploadResponse.contentUri
|
||||
} catch (t: Throwable) {
|
||||
Timber.e(t)
|
||||
return handleFailure(params, t)
|
||||
}
|
||||
}
|
||||
|
||||
val progressListener = object : ProgressRequestBody.Listener {
|
||||
|
@ -133,27 +129,26 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
|
|||
|
||||
var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null
|
||||
|
||||
return try {
|
||||
val contentUploadResponse = if (params.isRoomEncrypted) {
|
||||
Timber.v("Encrypt file")
|
||||
contentUploadStateTracker.setEncrypting(eventId)
|
||||
|
||||
MXEncryptedAttachments.encryptAttachment(FileInputStream(attachmentFile), attachment.mimeType)
|
||||
.flatMap { encryptionResult ->
|
||||
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)
|
||||
}
|
||||
|
||||
return contentUploadResponse
|
||||
.fold(
|
||||
{ handleFailure(params, it) },
|
||||
{ handleSuccess(params, it.contentUri, uploadedFileEncryptedFileInfo, uploadedThumbnailUrl, uploadedThumbnailEncryptedFileInfo) }
|
||||
)
|
||||
handleSuccess(params, contentUploadResponse.contentUri, uploadedFileEncryptedFileInfo, uploadedThumbnailUrl, uploadedThumbnailEncryptedFileInfo)
|
||||
} catch (t: Throwable) {
|
||||
Timber.e(t)
|
||||
handleFailure(params, t)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleFailure(params: Params, failure: Throwable): Result {
|
||||
|
|
|
@ -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<GetUploadCapabilitiesResult>
|
||||
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.session.homeserver
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities
|
||||
import im.vector.matrix.android.internal.database.model.HomeServerCapabilitiesEntity
|
||||
import im.vector.matrix.android.internal.database.query.getOrCreate
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import im.vector.matrix.android.internal.util.awaitTransaction
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface GetHomeServerCapabilitiesTask : Task<Unit, Unit>
|
||||
|
||||
internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
|
||||
private val capabilitiesAPI: CapabilitiesAPI,
|
||||
private val monarchy: Monarchy
|
||||
) : GetHomeServerCapabilitiesTask {
|
||||
|
||||
|
||||
override suspend fun execute(params: Unit) {
|
||||
var doRequest = false
|
||||
monarchy.awaitTransaction { realm ->
|
||||
val homeServerCapabilitiesEntity = HomeServerCapabilitiesEntity.getOrCreate(realm)
|
||||
|
||||
doRequest = homeServerCapabilitiesEntity.lastUpdatedTimestamp + MIN_DELAY_BETWEEN_TWO_REQUEST_MILLIS < Date().time
|
||||
}
|
||||
|
||||
if (!doRequest) {
|
||||
return
|
||||
}
|
||||
|
||||
val uploadCapabilities = executeRequest<GetUploadCapabilitiesResult> {
|
||||
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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.session.homeserver
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities
|
||||
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService
|
||||
import im.vector.matrix.android.internal.database.mapper.HomeServerCapabilitiesMapper
|
||||
import im.vector.matrix.android.internal.database.model.HomeServerCapabilitiesEntity
|
||||
import im.vector.matrix.android.internal.database.query.getOrCreate
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultHomeServerCapabilitiesService @Inject constructor(private val monarchy: Monarchy) : HomeServerCapabilitiesService {
|
||||
|
||||
override fun getHomeServerCapabilities(): HomeServerCapabilities {
|
||||
var entity: HomeServerCapabilitiesEntity? = null
|
||||
monarchy.doWithRealm { realm ->
|
||||
entity = HomeServerCapabilitiesEntity.getOrCreate(realm)
|
||||
}
|
||||
|
||||
return with(entity) {
|
||||
if (this != null) {
|
||||
HomeServerCapabilitiesMapper.map(this)
|
||||
} else {
|
||||
// Should not happen
|
||||
HomeServerCapabilities(HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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<String, String>()
|
||||
var timeout = 0L
|
||||
val token = syncTokenStore.getLastToken()
|
||||
|
|
|
@ -150,3 +150,9 @@ new Gson\(\)
|
|||
|
||||
### Use matrixOneTimeWorkRequestBuilder
|
||||
import androidx.work.OneTimeWorkRequestBuilder===1
|
||||
|
||||
### Use TextUtils.formatFileSize
|
||||
Formatter\.formatFileSize===1
|
||||
|
||||
### Use TextUtils.formatFileSize with short format param to true
|
||||
Formatter\.formatShortFileSize===1
|
||||
|
|
|
@ -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
|
||||
* ========================================================================================== */
|
||||
|
|
|
@ -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)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -27,7 +27,6 @@ import android.os.Bundle
|
|||
import android.os.Parcelable
|
||||
import android.text.Editable
|
||||
import android.text.Spannable
|
||||
import android.text.TextUtils
|
||||
import android.view.*
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.TextView
|
||||
|
@ -148,18 +147,15 @@ class RoomDetailFragment :
|
|||
* @param displayName the display name to sanitize
|
||||
* @return the sanitized display name
|
||||
*/
|
||||
fun sanitizeDisplayname(displayName: String): String? {
|
||||
// sanity checks
|
||||
if (!TextUtils.isEmpty(displayName)) {
|
||||
val ircPattern = " (IRC)"
|
||||
|
||||
private fun sanitizeDisplayName(displayName: String): String {
|
||||
if (displayName.endsWith(ircPattern)) {
|
||||
return displayName.substring(0, displayName.length - ircPattern.length)
|
||||
}
|
||||
}
|
||||
|
||||
return displayName
|
||||
}
|
||||
|
||||
private const val ircPattern = " (IRC)"
|
||||
}
|
||||
|
||||
private val roomDetailArgs: RoomDetailArgs by args()
|
||||
|
@ -227,6 +223,10 @@ class RoomDetailFragment :
|
|||
scrollOnHighlightedEventCallback.scheduleScrollTo(it)
|
||||
}
|
||||
|
||||
roomDetailViewModel.fileTooBigEvent.observeEvent(this) {
|
||||
displayFileTooBigWarning(it)
|
||||
}
|
||||
|
||||
roomDetailViewModel.selectSubscribe(this, RoomDetailViewState::tombstoneEventHandling, uniqueOnly("tombstoneEventHandling")) {
|
||||
renderTombstoneEventHandling(it)
|
||||
}
|
||||
|
@ -254,6 +254,18 @@ class RoomDetailFragment :
|
|||
}
|
||||
}
|
||||
|
||||
private fun displayFileTooBigWarning(error: FileTooBigError) {
|
||||
AlertDialog.Builder(requireActivity())
|
||||
.setTitle(R.string.dialog_title_error)
|
||||
.setMessage(getString(R.string.error_file_too_big,
|
||||
error.filename,
|
||||
TextUtils.formatFileSize(requireContext(), error.fileSizeInBytes),
|
||||
TextUtils.formatFileSize(requireContext(), error.homeServerLimitInBytes)
|
||||
))
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun setupNotificationView() {
|
||||
notificationAreaView.delegate = object : NotificationAreaView.Delegate {
|
||||
|
||||
|
@ -970,23 +982,23 @@ class RoomDetailFragment :
|
|||
// var vibrate = false
|
||||
|
||||
val myDisplayName = session.getUser(session.myUserId)?.displayName
|
||||
if (TextUtils.equals(myDisplayName, text)) {
|
||||
if (myDisplayName == text) {
|
||||
// current user
|
||||
if (TextUtils.isEmpty(composerLayout.composerEditText.text)) {
|
||||
if (composerLayout.composerEditText.text.isBlank()) {
|
||||
composerLayout.composerEditText.append(Command.EMOTE.command + " ")
|
||||
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
|
||||
// vibrate = true
|
||||
}
|
||||
} else {
|
||||
// another user
|
||||
if (TextUtils.isEmpty(composerLayout.composerEditText.text)) {
|
||||
if (composerLayout.composerEditText.text.isBlank()) {
|
||||
// Ensure displayName will not be interpreted as a Slash command
|
||||
if (text.startsWith("/")) {
|
||||
composerLayout.composerEditText.append("\\")
|
||||
}
|
||||
composerLayout.composerEditText.append(sanitizeDisplayname(text)!! + ": ")
|
||||
composerLayout.composerEditText.append(sanitizeDisplayName(text) + ": ")
|
||||
} else {
|
||||
composerLayout.composerEditText.text.insert(composerLayout.composerEditText.selectionStart, sanitizeDisplayname(text)!! + " ")
|
||||
composerLayout.composerEditText.text.insert(composerLayout.composerEditText.selectionStart, sanitizeDisplayName(text) + " ")
|
||||
}
|
||||
|
||||
// vibrate = true
|
||||
|
|
|
@ -37,6 +37,7 @@ import im.vector.matrix.android.api.session.events.model.isImageMessage
|
|||
import im.vector.matrix.android.api.session.events.model.isTextMessage
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.file.FileService
|
||||
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities
|
||||
import im.vector.matrix.android.api.session.room.model.Membership
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||
|
@ -48,6 +49,7 @@ import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent
|
|||
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
|
||||
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||
import im.vector.matrix.rx.rx
|
||||
import im.vector.riotx.BuildConfig
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.extensions.postLiveEvent
|
||||
import im.vector.riotx.core.intent.getFilenameFromUri
|
||||
|
@ -227,23 +229,22 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
val navigateToEvent: LiveData<LiveEvent<String>>
|
||||
get() = _navigateToEvent
|
||||
|
||||
private val _fileTooBigEvent = MutableLiveData<LiveEvent<FileTooBigError>>()
|
||||
val fileTooBigEvent: LiveData<LiveEvent<FileTooBigError>>
|
||||
get() = _fileTooBigEvent
|
||||
|
||||
private val _downloadedFileEvent = MutableLiveData<LiveEvent<DownloadFileState>>()
|
||||
val downloadedFileEvent: LiveData<LiveEvent<DownloadFileState>>
|
||||
get() = _downloadedFileEvent
|
||||
|
||||
|
||||
fun isMenuItemVisible(@IdRes itemId: Int): Boolean {
|
||||
if (itemId == R.id.clear_message_queue) {
|
||||
//For now always disable, woker cancellation is not working properly
|
||||
return false//timeline.pendingEventCount() > 0
|
||||
}
|
||||
if (itemId == R.id.resend_all) {
|
||||
return timeline.failedToDeliverEventCount() > 0
|
||||
}
|
||||
if (itemId == R.id.clear_all) {
|
||||
return timeline.failedToDeliverEventCount() > 0
|
||||
}
|
||||
return false
|
||||
fun isMenuItemVisible(@IdRes itemId: Int) = when (itemId) {
|
||||
R.id.clear_message_queue ->
|
||||
/* For now always disable on production, worker cancellation is not working properly */
|
||||
timeline.pendingEventCount() > 0 && BuildConfig.DEBUG
|
||||
R.id.resend_all -> timeline.failedToDeliverEventCount() > 0
|
||||
R.id.clear_all -> timeline.failedToDeliverEventCount() > 0
|
||||
else -> false
|
||||
}
|
||||
|
||||
// PRIVATE METHODS *****************************************************************************
|
||||
|
@ -470,7 +471,20 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
type = ContentAttachmentData.Type.values()[it.mediaType]
|
||||
)
|
||||
}
|
||||
|
||||
val homeServerCapabilities = session.getHomeServerCapabilities()
|
||||
|
||||
val maxUploadFileSize = homeServerCapabilities.maxUploadFileSize
|
||||
|
||||
if (maxUploadFileSize == HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN) {
|
||||
// Unknown limitation
|
||||
room.sendMedias(attachments)
|
||||
} else {
|
||||
when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) {
|
||||
null -> room.sendMedias(attachments)
|
||||
else -> _fileTooBigEvent.postValue(LiveEvent(FileTooBigError(tooBigFile.name ?: tooBigFile.path, tooBigFile.size, maxUploadFileSize)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleEventDisplayed(action: RoomDetailActions.EventDisplayed) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
|
||||
package im.vector.riotx.features.home.room.detail.timeline.helper
|
||||
|
||||
import android.text.format.Formatter
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ProgressBar
|
||||
|
@ -26,23 +25,25 @@ import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
|||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.di.ActiveSessionHolder
|
||||
import im.vector.riotx.core.error.ErrorFormatter
|
||||
import im.vector.riotx.core.resources.ColorProvider
|
||||
import im.vector.riotx.features.media.ImageContentRenderer
|
||||
import im.vector.riotx.core.utils.TextUtils
|
||||
import im.vector.riotx.features.ui.getMessageTextColor
|
||||
import javax.inject.Inject
|
||||
|
||||
class ContentUploadStateTrackerBinder @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
|
||||
private val colorProvider: ColorProvider) {
|
||||
private val colorProvider: ColorProvider,
|
||||
private val errorFormatter: ErrorFormatter) {
|
||||
|
||||
private val updateListeners = mutableMapOf<String, ContentUploadStateTracker.UpdateListener>()
|
||||
|
||||
fun bind(eventId: String,
|
||||
mediaData: ImageContentRenderer.Data,
|
||||
isLocalFile: Boolean,
|
||||
progressLayout: ViewGroup) {
|
||||
|
||||
activeSessionHolder.getActiveSession().also { session ->
|
||||
val uploadStateTracker = session.contentUploadProgressTracker()
|
||||
val updateListener = ContentMediaProgressUpdater(progressLayout, mediaData, colorProvider)
|
||||
val updateListener = ContentMediaProgressUpdater(progressLayout, isLocalFile, colorProvider, errorFormatter)
|
||||
updateListeners[eventId] = updateListener
|
||||
uploadStateTracker.track(eventId, updateListener)
|
||||
}
|
||||
|
@ -60,8 +61,9 @@ class ContentUploadStateTrackerBinder @Inject constructor(private val activeSess
|
|||
}
|
||||
|
||||
private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup,
|
||||
private val mediaData: ImageContentRenderer.Data,
|
||||
private val colorProvider: ColorProvider) : ContentUploadStateTracker.UpdateListener {
|
||||
private val isLocalFile: Boolean,
|
||||
private val colorProvider: ColorProvider,
|
||||
private val errorFormatter: ErrorFormatter) : ContentUploadStateTracker.UpdateListener {
|
||||
|
||||
override fun onUpdate(state: ContentUploadStateTracker.State) {
|
||||
when (state) {
|
||||
|
@ -76,7 +78,7 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup,
|
|||
}
|
||||
|
||||
private fun handleIdle(state: ContentUploadStateTracker.State.Idle) {
|
||||
if (mediaData.isLocalFile()) {
|
||||
if (isLocalFile) {
|
||||
progressLayout.isVisible = true
|
||||
val progressBar = progressLayout.findViewById<ProgressBar>(R.id.mediaProgressBar)
|
||||
val progressTextView = progressLayout.findViewById<TextView>(R.id.mediaProgressTextView)
|
||||
|
@ -124,8 +126,8 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup,
|
|||
progressBar?.isIndeterminate = false
|
||||
progressBar?.progress = percent.toInt()
|
||||
progressTextView?.text = progressLayout.context.getString(resId,
|
||||
Formatter.formatShortFileSize(progressLayout.context, current),
|
||||
Formatter.formatShortFileSize(progressLayout.context, total))
|
||||
TextUtils.formatFileSize(progressLayout.context, current, true),
|
||||
TextUtils.formatFileSize(progressLayout.context, total, true))
|
||||
progressTextView?.setTextColor(colorProvider.getMessageTextColor(SendState.SENDING))
|
||||
}
|
||||
|
||||
|
@ -134,7 +136,7 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup,
|
|||
val progressBar = progressLayout.findViewById<ProgressBar>(R.id.mediaProgressBar)
|
||||
val progressTextView = progressLayout.findViewById<TextView>(R.id.mediaProgressTextView)
|
||||
progressBar?.isVisible = false
|
||||
progressTextView?.text = state.throwable.localizedMessage
|
||||
progressTextView?.text = errorFormatter.toHumanReadable(state.throwable)
|
||||
progressTextView?.setTextColor(colorProvider.getMessageTextColor(SendState.UNDELIVERED))
|
||||
}
|
||||
|
||||
|
|
|
@ -111,8 +111,6 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
|||
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<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
|||
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()) {
|
||||
|
|
|
@ -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<MessageFileItem.Holder>() {
|
||||
|
@ -36,19 +38,35 @@ abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
|
|||
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<ViewGroup>(R.id.messageFileUploadProgressLayout)
|
||||
val fileLayout by bind<ViewGroup>(R.id.messageFileLayout)
|
||||
val fileImageView by bind<ImageView>(R.id.messageFileImageView)
|
||||
val filenameView by bind<TextView>(R.id.messageFilenameView)
|
||||
|
|
|
@ -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,7 +45,9 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
|
|||
super.bind(holder)
|
||||
imageContentRenderer.render(mediaData, ImageContentRenderer.Mode.THUMBNAIL, holder.imageView)
|
||||
if (!informationData.sendState.hasFailed()) {
|
||||
contentUploadStateTrackerBinder.bind(informationData.eventId, mediaData, holder.progressLayout)
|
||||
contentUploadStateTrackerBinder.bind(informationData.eventId, mediaData.isLocalFile(), holder.progressLayout)
|
||||
} else {
|
||||
holder.progressLayout.isVisible = false
|
||||
}
|
||||
holder.imageView.setOnClickListener(clickListener)
|
||||
holder.imageView.setOnLongClickListener(longClickListener)
|
||||
|
|
|
@ -33,9 +33,9 @@ import im.vector.riotx.core.di.ActiveSessionHolder
|
|||
import im.vector.riotx.core.glide.GlideApp
|
||||
import im.vector.riotx.core.glide.GlideRequest
|
||||
import im.vector.riotx.core.utils.DimensionConverter
|
||||
import im.vector.riotx.core.utils.isLocalFile
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
class ImageContentRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
|
||||
|
@ -54,9 +54,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
|
|||
val rotation: Int? = null
|
||||
) : Parcelable {
|
||||
|
||||
fun isLocalFile(): Boolean {
|
||||
return url != null && File(url).exists()
|
||||
}
|
||||
fun isLocalFile() = url.isLocalFile()
|
||||
}
|
||||
|
||||
enum class Mode {
|
||||
|
|
|
@ -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<Long>() {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,30 +2,21 @@
|
|||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/messageFileLayout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginRight="32dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:duplicateParentState="true"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/messageFilee2eIcon"
|
||||
android:layout_width="14dp"
|
||||
android:layout_height="14dp"
|
||||
android:src="@drawable/e2e_verified"
|
||||
android:visibility="gone" />
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<!-- the media type -->
|
||||
<ImageView
|
||||
|
@ -34,34 +25,45 @@
|
|||
android:layout_height="@dimen/chat_avatar_size"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginLeft="4dp"
|
||||
android:src="@drawable/filetype_image" />
|
||||
app:layout_constraintStart_toEndOf="@+id/messageFilee2eIcon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@drawable/filetype_image" />
|
||||
|
||||
<!-- the media -->
|
||||
<TextView
|
||||
android:id="@+id/messageFilenameView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="@dimen/chat_avatar_size"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginLeft="4dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:autoLink="none"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="@dimen/chat_avatar_size"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/messageFileImageView"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="A filename here" />
|
||||
|
||||
</LinearLayout>
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/horizontalBarrier"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="bottom"
|
||||
app:constraint_referenced_ids="messageFileImageView,messageFilenameView" />
|
||||
|
||||
<include
|
||||
android:id="@+id/messageMediaUploadProgressLayout"
|
||||
android:id="@+id/messageFileUploadProgressLayout"
|
||||
layout="@layout/media_upload_download_progress_layout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="46dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginRight="32dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/messageFileLayout"
|
||||
app:layout_constraintTop_toBottomOf="@+id/horizontalBarrier"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
<item
|
||||
android:id="@+id/clear_message_queue"
|
||||
android:title="@string/clear_timeline_send_queue"
|
||||
android:visible="false"
|
||||
android:visible="@bool/debug_mode"
|
||||
app:showAsAction="never"
|
||||
tools:visible="true" />
|
||||
|
||||
|
|
|
@ -22,4 +22,6 @@
|
|||
<string name="a11y_create_room">Create a new room</string>
|
||||
<string name="a11y_close_keys_backup_banner">Close keys backup banner</string>
|
||||
|
||||
<string name="error_file_too_big">"The file '%1$s' (%2$s) is too large to upload. The limit is %3$s."</string>
|
||||
|
||||
</resources>
|
Loading…
Reference in New Issue