Merge branch 'develop' into feature/read_marker

This commit is contained in:
ganfra 2019-09-26 12:19:40 +02:00
commit a3f561d788
59 changed files with 945 additions and 321 deletions

View File

@ -5,13 +5,14 @@ Features:
- -
Improvements: Improvements:
- - Persist active tab between sessions (#503)
- Do not upload file too big for the homeserver (#587)
Other changes: Other changes:
- -
Bugfix: Bugfix:
- - Fix issue on upload error in loop (#587)
Translations: Translations:
- -
@ -19,6 +20,12 @@ Translations:
Build: Build:
- -
Changes in RiotX 0.6.1 (2019-09-24)
===================================================
Bugfix:
- Fix crash: MergedHeaderItem was missing dimensionConverter
Changes in RiotX 0.6.0 (2019-09-24) Changes in RiotX 0.6.0 (2019-09-24)
=================================================== ===================================================

View File

@ -22,6 +22,7 @@ import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import okio.Buffer import okio.Buffer
import timber.log.Timber
import java.io.IOException import java.io.IOException
import java.nio.charset.Charset import java.nio.charset.Charset
import javax.inject.Inject import javax.inject.Inject
@ -58,15 +59,21 @@ internal class CurlLoggingInterceptor @Inject constructor(private val logger: Ht
val requestBody = request.body() val requestBody = request.body()
if (requestBody != null) { if (requestBody != null) {
val buffer = Buffer() if (requestBody.contentLength() > 100_000) {
requestBody.writeTo(buffer) Timber.w("Unable to log curl command data, size is too big (${requestBody.contentLength()})")
var charset: Charset? = UTF8 // Ensure the curl command will failed
val contentType = requestBody.contentType() curlCmd += "DATA IS TOO BIG"
if (contentType != null) { } else {
charset = contentType.charset(UTF8) 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() val headers = request.headers()

View File

@ -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.crypto.CryptoService
import im.vector.matrix.android.api.session.file.FileService 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.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.pushers.PushersService
import im.vector.matrix.android.api.session.room.RoomDirectoryService import im.vector.matrix.android.api.session.room.RoomDirectoryService
import im.vector.matrix.android.api.session.room.RoomService import im.vector.matrix.android.api.session.room.RoomService
@ -52,6 +53,7 @@ interface Session :
PushRuleService, PushRuleService,
PushersService, PushersService,
InitialSyncProgressService, InitialSyncProgressService,
HomeServerCapabilitiesService,
SecureStorageService { SecureStorageService {
/** /**

View File

@ -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
}
}

View File

@ -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
}

View File

@ -117,13 +117,13 @@ internal abstract class CryptoModule {
abstract fun bindGetDevicesTask(getDevicesTask: DefaultGetDevicesTask): GetDevicesTask abstract fun bindGetDevicesTask(getDevicesTask: DefaultGetDevicesTask): GetDevicesTask
@Binds @Binds
abstract fun bindSetDeviceNameTask(getDevicesTask: DefaultSetDeviceNameTask): SetDeviceNameTask abstract fun bindSetDeviceNameTask(setDeviceNameTask: DefaultSetDeviceNameTask): SetDeviceNameTask
@Binds @Binds
abstract fun bindUploadKeysTask(getDevicesTask: DefaultUploadKeysTask): UploadKeysTask abstract fun bindUploadKeysTask(uploadKeysTask: DefaultUploadKeysTask): UploadKeysTask
@Binds @Binds
abstract fun bindDownloadKeysForUsersTask(downloadKeysForUsers: DefaultDownloadKeysForUsers): DownloadKeysForUsersTask abstract fun bindDownloadKeysForUsersTask(downloadKeysForUsersTask: DefaultDownloadKeysForUsers): DownloadKeysForUsersTask
@Binds @Binds
abstract fun bindCreateKeysBackupVersionTask(createKeysBackupVersionTask: DefaultCreateKeysBackupVersionTask): CreateKeysBackupVersionTask abstract fun bindCreateKeysBackupVersionTask(createKeysBackupVersionTask: DefaultCreateKeysBackupVersionTask): CreateKeysBackupVersionTask
@ -135,10 +135,10 @@ internal abstract class CryptoModule {
abstract fun bindDeleteRoomSessionDataTask(deleteRoomSessionDataTask: DefaultDeleteRoomSessionDataTask): DeleteRoomSessionDataTask abstract fun bindDeleteRoomSessionDataTask(deleteRoomSessionDataTask: DefaultDeleteRoomSessionDataTask): DeleteRoomSessionDataTask
@Binds @Binds
abstract fun bindDeleteRoomSessionsDataTask(deleteRoomSessionDataTask: DefaultDeleteRoomSessionsDataTask): DeleteRoomSessionsDataTask abstract fun bindDeleteRoomSessionsDataTask(deleteRoomSessionsDataTask: DefaultDeleteRoomSessionsDataTask): DeleteRoomSessionsDataTask
@Binds @Binds
abstract fun bindDeleteSessionsDataTask(deleteRoomSessionDataTask: DefaultDeleteSessionsDataTask): DeleteSessionsDataTask abstract fun bindDeleteSessionsDataTask(deleteSessionsDataTask: DefaultDeleteSessionsDataTask): DeleteSessionsDataTask
@Binds @Binds
abstract fun bindGetKeysBackupLastVersionTask(getKeysBackupLastVersionTask: DefaultGetKeysBackupLastVersionTask): GetKeysBackupLastVersionTask abstract fun bindGetKeysBackupLastVersionTask(getKeysBackupLastVersionTask: DefaultGetKeysBackupLastVersionTask): GetKeysBackupLastVersionTask
@ -150,19 +150,19 @@ internal abstract class CryptoModule {
abstract fun bindGetRoomSessionDataTask(getRoomSessionDataTask: DefaultGetRoomSessionDataTask): GetRoomSessionDataTask abstract fun bindGetRoomSessionDataTask(getRoomSessionDataTask: DefaultGetRoomSessionDataTask): GetRoomSessionDataTask
@Binds @Binds
abstract fun bindGetRoomSessionsDataTask(getRoomSessionDataTask: DefaultGetRoomSessionsDataTask): GetRoomSessionsDataTask abstract fun bindGetRoomSessionsDataTask(getRoomSessionsDataTask: DefaultGetRoomSessionsDataTask): GetRoomSessionsDataTask
@Binds @Binds
abstract fun bindGetSessionsDataTask(getRoomSessionDataTask: DefaultGetSessionsDataTask): GetSessionsDataTask abstract fun bindGetSessionsDataTask(getSessionsDataTask: DefaultGetSessionsDataTask): GetSessionsDataTask
@Binds @Binds
abstract fun bindStoreRoomSessionDataTask(storeRoomSessionDataTask: DefaultStoreRoomSessionDataTask): StoreRoomSessionDataTask abstract fun bindStoreRoomSessionDataTask(storeRoomSessionDataTask: DefaultStoreRoomSessionDataTask): StoreRoomSessionDataTask
@Binds @Binds
abstract fun bindStoreRoomSessionsDataTask(storeRoomSessionDataTask: DefaultStoreRoomSessionsDataTask): StoreRoomSessionsDataTask abstract fun bindStoreRoomSessionsDataTask(storeRoomSessionsDataTask: DefaultStoreRoomSessionsDataTask): StoreRoomSessionsDataTask
@Binds @Binds
abstract fun bindStoreSessionsDataTask(storeRoomSessionDataTask: DefaultStoreSessionsDataTask): StoreSessionsDataTask abstract fun bindStoreSessionsDataTask(storeSessionsDataTask: DefaultStoreSessionsDataTask): StoreSessionsDataTask
@Binds @Binds
abstract fun bindUpdateKeysBackupVersionTask(updateKeysBackupVersionTask: DefaultUpdateKeysBackupVersionTask): UpdateKeysBackupVersionTask abstract fun bindUpdateKeysBackupVersionTask(updateKeysBackupVersionTask: DefaultUpdateKeysBackupVersionTask): UpdateKeysBackupVersionTask

View File

@ -17,7 +17,6 @@
package im.vector.matrix.android.internal.crypto.attachments package im.vector.matrix.android.internal.crypto.attachments
import android.util.Base64 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.EncryptedFileInfo
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileKey import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileKey
import timber.log.Timber import timber.log.Timber
@ -50,7 +49,7 @@ object MXEncryptedAttachments {
* @param mimetype the mime type * @param mimetype the mime type
* @return the encryption file info * @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 t0 = System.currentTimeMillis()
val secureRandom = SecureRandom() val secureRandom = SecureRandom()
@ -70,7 +69,7 @@ object MXEncryptedAttachments {
val outStream = ByteArrayOutputStream() val outStream = ByteArrayOutputStream()
try { outStream.use {
val encryptCipher = Cipher.getInstance(CIPHER_ALGORITHM) val encryptCipher = Cipher.getInstance(CIPHER_ALGORITHM)
val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM) val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM)
val ivParameterSpec = IvParameterSpec(initVectorBytes) val ivParameterSpec = IvParameterSpec(initVectorBytes)
@ -114,19 +113,7 @@ object MXEncryptedAttachments {
) )
Timber.v("Encrypt in ${System.currentTimeMillis() - t0} ms") Timber.v("Encrypt in ${System.currentTimeMillis() - t0} ms")
return Try.just(result) return 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")
}
} }
} }

View File

@ -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
)
}
}

View File

@ -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
}

View File

@ -46,6 +46,7 @@ import io.realm.annotations.RealmModule
ReadReceiptsSummaryEntity::class, ReadReceiptsSummaryEntity::class,
ReadMarkerEntity::class, ReadMarkerEntity::class,
UserDraftsEntity::class, UserDraftsEntity::class,
DraftEntity::class DraftEntity::class,
HomeServerCapabilitiesEntity::class
]) ])
internal class SessionRealmModule internal class SessionRealmModule

View File

@ -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
}

View File

@ -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_R0 = "$URI_API_PREFIX_PATH/r0/"
const val URI_API_PREFIX_PATH_UNSTABLE = "$URI_API_PREFIX_PATH/unstable/" 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/"
} }

View File

@ -16,24 +16,15 @@
package im.vector.matrix.android.internal.network 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.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.internal.di.MoshiProvider
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import okhttp3.ResponseBody
import org.greenrobot.eventbus.EventBus
import retrofit2.Call import retrofit2.Call
import timber.log.Timber
import java.io.IOException import java.io.IOException
internal suspend inline fun <DATA> executeRequest(block: Request<DATA>.() -> Unit) = Request<DATA>().apply(block).execute() internal suspend inline fun <DATA> executeRequest(block: Request<DATA>.() -> Unit) = Request<DATA>().apply(block).execute()
internal class Request<DATA> { internal class Request<DATA> {
private val moshi: Moshi = MoshiProvider.providesMoshi()
lateinit var apiCall: Call<DATA> lateinit var apiCall: Call<DATA>
suspend fun execute(): DATA { suspend fun execute(): DATA {
@ -43,7 +34,7 @@ internal class Request<DATA> {
response.body() response.body()
?: throw IllegalStateException("The request returned a null body") ?: throw IllegalStateException("The request returned a null body")
} else { } else {
throw manageFailure(response.errorBody(), response.code()) throw response.toFailure()
} }
} catch (exception: Throwable) { } catch (exception: Throwable) {
throw when (exception) { 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)
}
} }

View File

@ -18,14 +18,23 @@
package im.vector.matrix.android.internal.network 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 kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.ResponseBody
import org.greenrobot.eventbus.EventBus
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
import timber.log.Timber
import java.io.IOException
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
suspend fun <T> Call<T>.awaitResponse(): Response<T> { internal suspend fun <T> Call<T>.awaitResponse(): Response<T> {
return suspendCancellableCoroutine { continuation -> return suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation { continuation.invokeOnCancellation {
cancel() cancel()
@ -40,4 +49,64 @@ 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)
}

View File

@ -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.crypto.CryptoService
import im.vector.matrix.android.api.session.file.FileService 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.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.pushers.PushersService
import im.vector.matrix.android.api.session.room.RoomDirectoryService import im.vector.matrix.android.api.session.room.RoomDirectoryService
import im.vector.matrix.android.api.session.room.RoomService 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 syncThreadProvider: Provider<SyncThread>,
private val contentUrlResolver: ContentUrlResolver, private val contentUrlResolver: ContentUrlResolver,
private val contentUploadProgressTracker: ContentUploadStateTracker, private val contentUploadProgressTracker: ContentUploadStateTracker,
private val initialSyncProgressService: Lazy<InitialSyncProgressService>) private val initialSyncProgressService: Lazy<InitialSyncProgressService>,
private val homeServerCapabilitiesService: Lazy<HomeServerCapabilitiesService>)
: Session, : Session,
RoomService by roomService.get(), RoomService by roomService.get(),
RoomDirectoryService by roomDirectoryService.get(), RoomDirectoryService by roomDirectoryService.get(),
@ -81,7 +83,8 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
PushersService by pushersService.get(), PushersService by pushersService.get(),
FileService by fileService.get(), FileService by fileService.get(),
InitialSyncProgressService by initialSyncProgressService.get(), InitialSyncProgressService by initialSyncProgressService.get(),
SecureStorageService by secureStorageService.get() { SecureStorageService by secureStorageService.get(),
HomeServerCapabilitiesService by homeServerCapabilitiesService.get() {
private var isOpen = false private var isOpen = false

View File

@ -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.filter.FilterModule
import im.vector.matrix.android.internal.session.group.GetGroupDataWorker 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.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.AddHttpPusherWorker
import im.vector.matrix.android.internal.session.pushers.PushersModule import im.vector.matrix.android.internal.session.pushers.PushersModule
import im.vector.matrix.android.internal.session.room.RoomModule import im.vector.matrix.android.internal.session.room.RoomModule
@ -51,6 +52,7 @@ import im.vector.matrix.android.internal.task.TaskExecutor
SessionModule::class, SessionModule::class,
RoomModule::class, RoomModule::class,
SyncModule::class, SyncModule::class,
HomeServerCapabilitiesModule::class,
SignOutModule::class, SignOutModule::class,
GroupModule::class, GroupModule::class,
UserModule::class, UserModule::class,

View File

@ -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.auth.data.SessionParams
import im.vector.matrix.android.api.session.InitialSyncProgressService import im.vector.matrix.android.api.session.InitialSyncProgressService
import im.vector.matrix.android.api.session.Session 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.api.session.securestorage.SecureStorageService
import im.vector.matrix.android.internal.database.LiveEntityObserver import im.vector.matrix.android.internal.database.LiveEntityObserver
import im.vector.matrix.android.internal.database.RealmKeysUtils 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.RetrofitFactory
import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor 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.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.EventRelationsAggregationUpdater
import im.vector.matrix.android.internal.session.room.create.RoomCreateEventLiveObserver import im.vector.matrix.android.internal.session.room.create.RoomCreateEventLiveObserver
import im.vector.matrix.android.internal.session.room.prune.EventsPruner import im.vector.matrix.android.internal.session.room.prune.EventsPruner
@ -162,7 +164,7 @@ internal abstract class SessionModule {
@Binds @Binds
@IntoSet @IntoSet
abstract fun bindEventRelationsAggregationUpdater(groupSummaryUpdater: EventRelationsAggregationUpdater): LiveEntityObserver abstract fun bindEventRelationsAggregationUpdater(eventRelationsAggregationUpdater: EventRelationsAggregationUpdater): LiveEntityObserver
@Binds @Binds
@IntoSet @IntoSet
@ -178,4 +180,7 @@ internal abstract class SessionModule {
@Binds @Binds
abstract fun bindSecureStorageService(secureStorageService: DefaultSecureStorageService): SecureStorageService abstract fun bindSecureStorageService(secureStorageService: DefaultSecureStorageService): SecureStorageService
@Binds
abstract fun bindHomeServerCapabilitiesService(homeServerCapabilitiesService: DefaultHomeServerCapabilitiesService): HomeServerCapabilitiesService
} }

View File

@ -16,12 +16,12 @@
package im.vector.matrix.android.internal.session.content package im.vector.matrix.android.internal.session.content
import arrow.core.Try
import arrow.core.Try.Companion.raise
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.internal.di.Authenticated import im.vector.matrix.android.internal.di.Authenticated
import im.vector.matrix.android.internal.network.ProgressRequestBody 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 okhttp3.*
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
@ -37,28 +37,26 @@ internal class FileUploader @Inject constructor(@Authenticated
private val responseAdapter = moshi.adapter(ContentUploadResponse::class.java) private val responseAdapter = moshi.adapter(ContentUploadResponse::class.java)
fun uploadFile(file: File, suspend fun uploadFile(file: File,
filename: String?, filename: String?,
mimeType: String, mimeType: String,
progressListener: ProgressRequestBody.Listener? = null): Try<ContentUploadResponse> { progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse {
val uploadBody = RequestBody.create(MediaType.parse(mimeType), file) val uploadBody = RequestBody.create(MediaType.parse(mimeType), file)
return upload(uploadBody, filename, progressListener) return upload(uploadBody, filename, progressListener)
} }
fun uploadByteArray(byteArray: ByteArray, suspend fun uploadByteArray(byteArray: ByteArray,
filename: String?, filename: String?,
mimeType: String, mimeType: String,
progressListener: ProgressRequestBody.Listener? = null): Try<ContentUploadResponse> { progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse {
val uploadBody = RequestBody.create(MediaType.parse(mimeType), byteArray) val uploadBody = RequestBody.create(MediaType.parse(mimeType), byteArray)
return upload(uploadBody, filename, progressListener) return upload(uploadBody, filename, progressListener)
} }
private fun upload(uploadBody: RequestBody, filename: String?, progressListener: ProgressRequestBody.Listener?): Try<ContentUploadResponse> { private suspend fun upload(uploadBody: RequestBody, filename: String?, progressListener: ProgressRequestBody.Listener?): ContentUploadResponse {
val urlBuilder = HttpUrl.parse(uploadUrl)?.newBuilder() ?: return raise(RuntimeException()) val urlBuilder = HttpUrl.parse(uploadUrl)?.newBuilder() ?: throw RuntimeException()
val httpUrl = urlBuilder val httpUrl = urlBuilder
.addQueryParameter("filename", filename) .addQueryParameter("filename", filename)
@ -71,19 +69,15 @@ internal class FileUploader @Inject constructor(@Authenticated
.post(requestBody) .post(requestBody)
.build() .build()
return Try { return okHttpClient.newCall(request).awaitResponse().use { response ->
okHttpClient.newCall(request).execute().use { response -> if (!response.isSuccessful) {
if (!response.isSuccessful) { throw response.toFailure()
throw IOException() } else {
} else { response.body()?.source()?.let {
response.body()?.source()?.let { responseAdapter.fromJson(it)
responseAdapter.fromJson(it)
}
?: throw IOException()
} }
?: throw IOException()
} }
} }
} }
} }

View File

@ -93,32 +93,28 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
} }
} }
val contentUploadResponse = if (params.isRoomEncrypted) { try {
Timber.v("Encrypt thumbnail") val contentUploadResponse = if (params.isRoomEncrypted) {
contentUploadStateTracker.setEncryptingThumbnail(eventId) Timber.v("Encrypt thumbnail")
MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType) contentUploadStateTracker.setEncryptingThumbnail(eventId)
.flatMap { encryptionResult -> val encryptionResult = MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType)
uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo 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 uploadedThumbnailUrl = contentUploadResponse.contentUri
.uploadByteArray(encryptionResult.encryptedByteArray, } catch (t: Throwable) {
"thumb_${attachment.name}", Timber.e(t)
"application/octet-stream", return handleFailure(params, t)
thumbnailProgressListener)
}
} else {
fileUploader
.uploadByteArray(thumbnailData.bytes,
"thumb_${attachment.name}",
thumbnailData.mimeType,
thumbnailProgressListener)
} }
contentUploadResponse
.fold(
{ Timber.e(it) },
{ uploadedThumbnailUrl = it.contentUri }
)
} }
val progressListener = object : ProgressRequestBody.Listener { val progressListener = object : ProgressRequestBody.Listener {
@ -133,27 +129,26 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null
val contentUploadResponse = if (params.isRoomEncrypted) { return try {
Timber.v("Encrypt file") val contentUploadResponse = if (params.isRoomEncrypted) {
contentUploadStateTracker.setEncrypting(eventId) Timber.v("Encrypt file")
contentUploadStateTracker.setEncrypting(eventId)
MXEncryptedAttachments.encryptAttachment(FileInputStream(attachmentFile), attachment.mimeType) val encryptionResult = MXEncryptedAttachments.encryptAttachment(FileInputStream(attachmentFile), attachment.mimeType)
.flatMap { encryptionResult -> uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo
uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo
fileUploader fileUploader
.uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener) .uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener)
} } else {
} else { fileUploader
fileUploader .uploadFile(attachmentFile, attachment.name, attachment.mimeType, progressListener)
.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 { private fun handleFailure(params: Params, failure: Throwable): Result {

View File

@ -43,7 +43,7 @@ internal abstract class FilterModule {
abstract fun bindFilterService(filterService: DefaultFilterService): FilterService abstract fun bindFilterService(filterService: DefaultFilterService): FilterService
@Binds @Binds
abstract fun bindSaveFilterTask(saveFilterTask_Factory: DefaultSaveFilterTask): SaveFilterTask abstract fun bindSaveFilterTask(saveFilterTask: DefaultSaveFilterTask): SaveFilterTask
} }

View File

@ -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>
}

View File

@ -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
}
}

View File

@ -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)
}
}
}
}

View File

@ -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
)

View File

@ -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
}

View File

@ -26,8 +26,8 @@ import javax.inject.Inject
internal interface GetPushersTask : Task<Unit, Unit> internal interface GetPushersTask : Task<Unit, Unit>
internal class DefaultGetPusherTask @Inject constructor(private val pushersAPI: PushersAPI, internal class DefaultGetPushersTask @Inject constructor(private val pushersAPI: PushersAPI,
private val monarchy: Monarchy) : GetPushersTask { private val monarchy: Monarchy) : GetPushersTask {
override suspend fun execute(params: Unit) { override suspend fun execute(params: Unit) {
val response = executeRequest<GetPushersResponse> { val response = executeRequest<GetPushersResponse> {

View File

@ -54,7 +54,7 @@ internal abstract class PushersModule {
abstract fun bindConditionResolver(conditionResolver: DefaultConditionResolver): ConditionResolver abstract fun bindConditionResolver(conditionResolver: DefaultConditionResolver): ConditionResolver
@Binds @Binds
abstract fun bindGetPushersTask(getPusherTask: DefaultGetPusherTask): GetPushersTask abstract fun bindGetPushersTask(getPushersTask: DefaultGetPushersTask): GetPushersTask
@Binds @Binds
abstract fun bindGetPushRulesTask(getPushRulesTask: DefaultGetPushRulesTask): GetPushRulesTask abstract fun bindGetPushRulesTask(getPushRulesTask: DefaultGetPushRulesTask): GetPushRulesTask

View File

@ -127,5 +127,5 @@ internal abstract class RoomModule {
abstract fun bindFileService(fileService: DefaultFileService): FileService abstract fun bindFileService(fileService: DefaultFileService): FileService
@Binds @Binds
abstract fun bindFetchEditHistoryTask(editHistoryTask: DefaultFetchEditHistoryTask): FetchEditHistoryTask abstract fun bindFetchEditHistoryTask(fetchEditHistoryTask: DefaultFetchEditHistoryTask): FetchEditHistoryTask
} }

View File

@ -16,7 +16,6 @@
package im.vector.matrix.android.internal.session.sync package im.vector.matrix.android.internal.session.sync
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.R import im.vector.matrix.android.R
import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError 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.network.executeRequest
import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService
import im.vector.matrix.android.internal.session.filter.FilterRepository 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.session.sync.model.SyncResponse
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import javax.inject.Inject import javax.inject.Inject
@ -42,11 +42,14 @@ internal class DefaultSyncTask @Inject constructor(private val syncAPI: SyncAPI,
private val sessionParamsStore: SessionParamsStore, private val sessionParamsStore: SessionParamsStore,
private val initialSyncProgressService: DefaultInitialSyncProgressService, private val initialSyncProgressService: DefaultInitialSyncProgressService,
private val syncTokenStore: SyncTokenStore, private val syncTokenStore: SyncTokenStore,
private val monarchy: Monarchy private val getHomeServerCapabilitiesTask: GetHomeServerCapabilitiesTask
) : SyncTask { ) : SyncTask {
override suspend fun execute(params: SyncTask.Params) { override suspend fun execute(params: SyncTask.Params) {
// Maybe refresh the home server capabilities data we know
getHomeServerCapabilitiesTask.execute(Unit)
val requestParams = HashMap<String, String>() val requestParams = HashMap<String, String>()
var timeout = 0L var timeout = 0L
val token = syncTokenStore.getLastToken() val token = syncTokenStore.getLastToken()

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Note to translator: please add here only the string which are different than default version (which is en-rGB) -->
<string name="verification_emoji_wrench">Wrench</string>
<string name="verification_emoji_airplane">Airplane</string>
</resources>

View File

@ -149,4 +149,10 @@ android\.app\.AlertDialog
new Gson\(\) new Gson\(\)
### Use matrixOneTimeWorkRequestBuilder ### Use matrixOneTimeWorkRequestBuilder
import androidx.work.OneTimeWorkRequestBuilder===1 import androidx.work.OneTimeWorkRequestBuilder===1
### Use TextUtils.formatFileSize
Formatter\.formatFileSize===1
### Use TextUtils.formatFileSize with short format param to true
Formatter\.formatShortFileSize===1

View File

@ -32,6 +32,7 @@ cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-da/strings.xml ./mat
cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-de/strings.xml ./matrix-sdk-android/src/main/res/values-de/strings.xml cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-de/strings.xml ./matrix-sdk-android/src/main/res/values-de/strings.xml
cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-el/strings.xml ./matrix-sdk-android/src/main/res/values-el/strings.xml cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-el/strings.xml ./matrix-sdk-android/src/main/res/values-el/strings.xml
cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-eo/strings.xml ./matrix-sdk-android/src/main/res/values-eo/strings.xml cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-eo/strings.xml ./matrix-sdk-android/src/main/res/values-eo/strings.xml
cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-en-rUS/strings.xml ./matrix-sdk-android/src/main/res/values-en-rUS/strings.xml
cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-es/strings.xml ./matrix-sdk-android/src/main/res/values-es/strings.xml cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-es/strings.xml ./matrix-sdk-android/src/main/res/values-es/strings.xml
cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-es-rMX/strings.xml ./matrix-sdk-android/src/main/res/values-es-rMX/strings.xml cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-es-rMX/strings.xml ./matrix-sdk-android/src/main/res/values-es-rMX/strings.xml
cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-eu/strings.xml ./matrix-sdk-android/src/main/res/values-eu/strings.xml cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-eu/strings.xml ./matrix-sdk-android/src/main/res/values-eu/strings.xml

View File

@ -65,6 +65,7 @@ import im.vector.riotx.features.roomdirectory.picker.RoomDirectoryPickerFragment
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFragment import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFragment
import im.vector.riotx.features.settings.* import im.vector.riotx.features.settings.*
import im.vector.riotx.features.settings.push.PushGatewaysFragment import im.vector.riotx.features.settings.push.PushGatewaysFragment
import im.vector.riotx.features.ui.UiStateRepository
@Component(dependencies = [VectorComponent::class], modules = [AssistedInjectModule::class, ViewModelModule::class, HomeModule::class]) @Component(dependencies = [VectorComponent::class], modules = [AssistedInjectModule::class, ViewModelModule::class, HomeModule::class])
@ScreenScope @ScreenScope
@ -80,6 +81,8 @@ interface ScreenComponent {
fun navigator(): Navigator fun navigator(): Navigator
fun uiStateRepository(): UiStateRepository
fun inject(activity: HomeActivity) fun inject(activity: HomeActivity)
fun inject(roomDetailFragment: RoomDetailFragment) fun inject(roomDetailFragment: RoomDetailFragment)

View File

@ -42,13 +42,14 @@ import im.vector.riotx.features.rageshake.BugReporter
import im.vector.riotx.features.rageshake.VectorFileLogger import im.vector.riotx.features.rageshake.VectorFileLogger
import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.settings.VectorPreferences
import im.vector.riotx.features.ui.UiStateRepository
import javax.inject.Singleton import javax.inject.Singleton
@Component(modules = [VectorModule::class]) @Component(modules = [VectorModule::class])
@Singleton @Singleton
interface VectorComponent { interface VectorComponent {
fun inject(vectorApplication: NotificationBroadcastReceiver) fun inject(notificationBroadcastReceiver: NotificationBroadcastReceiver)
fun inject(vectorApplication: VectorApplication) fun inject(vectorApplication: VectorApplication)
@ -64,7 +65,7 @@ interface VectorComponent {
fun resources(): Resources fun resources(): Resources
fun dimensionUtils(): DimensionConverter fun dimensionConverter(): DimensionConverter
fun vectorConfiguration(): VectorConfiguration fun vectorConfiguration(): VectorConfiguration
@ -106,6 +107,8 @@ interface VectorComponent {
fun vectorFileLogger(): VectorFileLogger fun vectorFileLogger(): VectorFileLogger
fun uiStateRepository(): UiStateRepository
@Component.Factory @Component.Factory
interface Factory { interface Factory {
fun create(@BindsInstance context: Context): VectorComponent fun create(@BindsInstance context: Context): VectorComponent

View File

@ -28,6 +28,8 @@ import im.vector.matrix.android.api.auth.Authenticator
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.riotx.features.navigation.DefaultNavigator import im.vector.riotx.features.navigation.DefaultNavigator
import im.vector.riotx.features.navigation.Navigator import im.vector.riotx.features.navigation.Navigator
import im.vector.riotx.features.ui.SharedPreferencesUiStateRepository
import im.vector.riotx.features.ui.UiStateRepository
@Module @Module
abstract class VectorModule { abstract class VectorModule {
@ -62,7 +64,7 @@ abstract class VectorModule {
@Provides @Provides
@JvmStatic @JvmStatic
fun providesAuthenticator(matrix: Matrix): Authenticator{ fun providesAuthenticator(matrix: Matrix): Authenticator {
return matrix.authenticator() return matrix.authenticator()
} }
} }
@ -70,5 +72,7 @@ abstract class VectorModule {
@Binds @Binds
abstract fun bindNavigator(navigator: DefaultNavigator): Navigator abstract fun bindNavigator(navigator: DefaultNavigator): Navigator
@Binds
abstract fun bindUiStateRepository(uiStateRepository: SharedPreferencesUiStateRepository): UiStateRepository
} }

View File

@ -24,6 +24,8 @@ import java.io.File
// Implementation should return true in case of success // Implementation should return true in case of success
typealias ActionOnFile = (file: File) -> Boolean typealias ActionOnFile = (file: File) -> Boolean
internal fun String?.isLocalFile() = this != null && File(this).exists()
/* ========================================================================================== /* ==========================================================================================
* Delete * Delete
* ========================================================================================== */ * ========================================================================================== */

View File

@ -16,6 +16,9 @@
package im.vector.riotx.core.utils package im.vector.riotx.core.utils
import android.content.Context
import android.os.Build
import android.text.format.Formatter
import java.util.* import java.util.*
object TextUtils { object TextUtils {
@ -42,4 +45,28 @@ object TextUtils {
return value.toString() 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)
}
}
} }

View File

@ -16,7 +16,6 @@
package im.vector.riotx.features.home package im.vector.riotx.features.home
import android.app.ProgressDialog
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
@ -66,8 +65,6 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
@Inject lateinit var pushManager: PushersManager @Inject lateinit var pushManager: PushersManager
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager @Inject lateinit var notificationDrawerManager: NotificationDrawerManager
private var progress: ProgressDialog? = null
private val drawerListener = object : DrawerLayout.SimpleDrawerListener() { private val drawerListener = object : DrawerLayout.SimpleDrawerListener() {
override fun onDrawerStateChanged(newState: Int) { override fun onDrawerStateChanged(newState: Int) {
hideKeyboard() hideKeyboard()
@ -93,18 +90,6 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
replaceFragment(homeDrawerFragment, R.id.homeDrawerFragmentContainer) replaceFragment(homeDrawerFragment, R.id.homeDrawerFragmentContainer)
} }
homeActivityViewModel.isLoading.observe(this, Observer<Boolean> {
// TODO better UI
if (it) {
progress?.dismiss()
progress = ProgressDialog(this)
progress?.setMessage(getString(R.string.room_recents_create_room))
progress?.show()
} else {
progress?.dismiss()
}
})
navigationViewModel.navigateTo.observeEvent(this) { navigation -> navigationViewModel.navigateTo.observeEvent(this) { navigation ->
when (navigation) { when (navigation) {
is Navigation.OpenDrawer -> drawerLayout.openDrawer(GravityCompat.START) is Navigation.OpenDrawer -> drawerLayout.openDrawer(GravityCompat.START)

View File

@ -16,8 +16,6 @@
package im.vector.riotx.features.home package im.vector.riotx.features.home
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import arrow.core.Option import arrow.core.Option
import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
@ -25,11 +23,9 @@ import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.features.home.group.ALL_COMMUNITIES_GROUP_ID import im.vector.riotx.features.home.group.ALL_COMMUNITIES_GROUP_ID
@ -61,10 +57,6 @@ class HomeActivityViewModel @AssistedInject constructor(@Assisted initialState:
} }
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean>
get() = _isLoading
init { init {
session.addListener(this) session.addListener(this)
observeRoomAndGroup() observeRoomAndGroup()
@ -93,7 +85,7 @@ class HomeActivityViewModel @AssistedInject constructor(@Assisted initialState:
.filter { !it.isDirect } .filter { !it.isDirect }
.filter { .filter {
selectedGroup?.groupId == ALL_COMMUNITIES_GROUP_ID selectedGroup?.groupId == ALL_COMMUNITIES_GROUP_ID
|| selectedGroup?.roomIds?.contains(it.roomId) ?: true || selectedGroup?.roomIds?.contains(it.roomId) ?: true
} }
filteredDirectRooms + filteredGroupRooms filteredDirectRooms + filteredGroupRooms
} }
@ -104,21 +96,6 @@ class HomeActivityViewModel @AssistedInject constructor(@Assisted initialState:
.disposeOnClear() .disposeOnClear()
} }
fun createRoom(createRoomParams: CreateRoomParams = CreateRoomParams()) {
_isLoading.value = true
session.createRoom(createRoomParams, object : MatrixCallback<String> {
override fun onSuccess(data: String) {
_isLoading.value = false
}
override fun onFailure(failure: Throwable) {
_isLoading.value = false
super.onFailure(failure)
}
})
}
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()

View File

@ -51,8 +51,6 @@ data class HomeDetailParams(
) : Parcelable ) : Parcelable
private const val CURRENT_DISPLAY_MODE = "CURRENT_DISPLAY_MODE"
private const val INDEX_CATCHUP = 0 private const val INDEX_CATCHUP = 0
private const val INDEX_PEOPLE = 1 private const val INDEX_PEOPLE = 1
private const val INDEX_ROOMS = 2 private const val INDEX_ROOMS = 2
@ -61,7 +59,6 @@ class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate {
private val params: HomeDetailParams by args() private val params: HomeDetailParams by args()
private val unreadCounterBadgeViews = arrayListOf<UnreadCounterBadgeView>() private val unreadCounterBadgeViews = arrayListOf<UnreadCounterBadgeView>()
private lateinit var currentDisplayMode: RoomListFragment.DisplayMode
private val viewModel: HomeDetailViewModel by fragmentViewModel() private val viewModel: HomeDetailViewModel by fragmentViewModel()
private lateinit var navigationViewModel: HomeNavigationViewModel private lateinit var navigationViewModel: HomeNavigationViewModel
@ -80,15 +77,16 @@ class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate {
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
currentDisplayMode = savedInstanceState?.getSerializable(CURRENT_DISPLAY_MODE) as? RoomListFragment.DisplayMode
?: RoomListFragment.DisplayMode.HOME
navigationViewModel = ViewModelProviders.of(requireActivity()).get(HomeNavigationViewModel::class.java) navigationViewModel = ViewModelProviders.of(requireActivity()).get(HomeNavigationViewModel::class.java)
switchDisplayMode(currentDisplayMode)
setupBottomNavigationView() setupBottomNavigationView()
setupToolbar() setupToolbar()
setupKeysBackupBanner() setupKeysBackupBanner()
viewModel.selectSubscribe(this, HomeDetailViewState::displayMode) { displayMode ->
switchDisplayMode(displayMode)
}
} }
private fun setupKeysBackupBanner() { private fun setupKeysBackupBanner() {
@ -126,11 +124,6 @@ class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate {
} }
override fun onSaveInstanceState(outState: Bundle) {
outState.putSerializable(CURRENT_DISPLAY_MODE, currentDisplayMode)
super.onSaveInstanceState(outState)
}
private fun setupToolbar() { private fun setupToolbar() {
val parentActivity = vectorBaseActivity val parentActivity = vectorBaseActivity
if (parentActivity is ToolbarConfigurable) { if (parentActivity is ToolbarConfigurable) {
@ -156,10 +149,7 @@ class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate {
R.id.bottom_action_rooms -> RoomListFragment.DisplayMode.ROOMS R.id.bottom_action_rooms -> RoomListFragment.DisplayMode.ROOMS
else -> RoomListFragment.DisplayMode.HOME else -> RoomListFragment.DisplayMode.HOME
} }
if (currentDisplayMode != displayMode) { viewModel.switchDisplayMode(displayMode)
currentDisplayMode = displayMode
switchDisplayMode(displayMode)
}
true true
} }
@ -176,6 +166,12 @@ class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate {
private fun switchDisplayMode(displayMode: RoomListFragment.DisplayMode) { private fun switchDisplayMode(displayMode: RoomListFragment.DisplayMode) {
groupToolbarTitleView.setText(displayMode.titleRes) groupToolbarTitleView.setText(displayMode.titleRes)
updateSelectedFragment(displayMode) updateSelectedFragment(displayMode)
// Update the navigation view (for when we restore the tabs)
bottomNavigationView.selectedItemId = when (displayMode) {
RoomListFragment.DisplayMode.PEOPLE -> R.id.bottom_action_people
RoomListFragment.DisplayMode.ROOMS -> R.id.bottom_action_rooms
else -> R.id.bottom_action_home
}
} }
private fun updateSelectedFragment(displayMode: RoomListFragment.DisplayMode) { private fun updateSelectedFragment(displayMode: RoomListFragment.DisplayMode) {

View File

@ -23,14 +23,19 @@ import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.riotx.core.di.HasScreenInjector
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.features.home.room.list.RoomListFragment
import im.vector.riotx.features.ui.UiStateRepository
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
/** /**
* View model used to update the home bottom bar notification counts * View model used to update the home bottom bar notification counts, observe the sync state and
* change the selected room list view
*/ */
class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: HomeDetailViewState, class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: HomeDetailViewState,
private val session: Session, private val session: Session,
private val uiStateRepository: UiStateRepository,
private val homeRoomListStore: HomeRoomListObservableStore) private val homeRoomListStore: HomeRoomListObservableStore)
: VectorViewModel<HomeDetailViewState>(initialState) { : VectorViewModel<HomeDetailViewState>(initialState) {
@ -41,6 +46,13 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho
companion object : MvRxViewModelFactory<HomeDetailViewModel, HomeDetailViewState> { companion object : MvRxViewModelFactory<HomeDetailViewModel, HomeDetailViewState> {
override fun initialState(viewModelContext: ViewModelContext): HomeDetailViewState? {
val uiStateRepository = (viewModelContext.activity as HasScreenInjector).injector().uiStateRepository()
return HomeDetailViewState(
displayMode = uiStateRepository.getDisplayMode()
)
}
@JvmStatic @JvmStatic
override fun create(viewModelContext: ViewModelContext, state: HomeDetailViewState): HomeDetailViewModel? { override fun create(viewModelContext: ViewModelContext, state: HomeDetailViewState): HomeDetailViewModel? {
val fragment: HomeDetailFragment = (viewModelContext as FragmentViewModelContext).fragment() val fragment: HomeDetailFragment = (viewModelContext as FragmentViewModelContext).fragment()
@ -53,6 +65,16 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho
observeRoomSummaries() observeRoomSummaries()
} }
fun switchDisplayMode(displayMode: RoomListFragment.DisplayMode) = withState { state ->
if (state.displayMode != displayMode) {
setState {
copy(displayMode = displayMode)
}
uiStateRepository.storeDisplayMode(displayMode)
}
}
// PRIVATE METHODS ***************************************************************************** // PRIVATE METHODS *****************************************************************************
private fun observeSyncState() { private fun observeSyncState() {

View File

@ -18,8 +18,10 @@ package im.vector.riotx.features.home
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
import im.vector.matrix.android.api.session.sync.SyncState import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.riotx.features.home.room.list.RoomListFragment
data class HomeDetailViewState( data class HomeDetailViewState(
val displayMode: RoomListFragment.DisplayMode = RoomListFragment.DisplayMode.HOME,
val notificationCountCatchup: Int = 0, val notificationCountCatchup: Int = 0,
val notificationHighlightCatchup: Boolean = false, val notificationHighlightCatchup: Boolean = false,
val notificationCountPeople: Int = 0, val notificationCountPeople: Int = 0,

View File

@ -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
)

View File

@ -27,7 +27,6 @@ import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.text.Editable import android.text.Editable
import android.text.Spannable import android.text.Spannable
import android.text.TextUtils
import android.view.* import android.view.*
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.TextView import android.widget.TextView
@ -79,20 +78,9 @@ import im.vector.riotx.core.glide.GlideApp
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.ui.views.JumpToReadMarkerView import im.vector.riotx.core.ui.views.JumpToReadMarkerView
import im.vector.riotx.core.ui.views.NotificationAreaView import im.vector.riotx.core.ui.views.NotificationAreaView
import im.vector.riotx.core.utils.*
import im.vector.riotx.core.utils.Debouncer import im.vector.riotx.core.utils.Debouncer
import im.vector.riotx.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_DOWNLOAD_FILE
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_CAMERA
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_VIDEO_CAMERA
import im.vector.riotx.core.utils.allGranted
import im.vector.riotx.core.utils.checkPermissions
import im.vector.riotx.core.utils.copyToClipboard
import im.vector.riotx.core.utils.createUIHandler import im.vector.riotx.core.utils.createUIHandler
import im.vector.riotx.core.utils.openCamera
import im.vector.riotx.core.utils.shareMedia
import im.vector.riotx.core.utils.toast
import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter
import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy
import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter
@ -163,18 +151,15 @@ class RoomDetailFragment :
* @param displayName the display name to sanitize * @param displayName the display name to sanitize
* @return the sanitized display name * @return the sanitized display name
*/ */
fun sanitizeDisplayname(displayName: String): String? { private fun sanitizeDisplayName(displayName: String): String {
// sanity checks if (displayName.endsWith(ircPattern)) {
if (!TextUtils.isEmpty(displayName)) { return displayName.substring(0, displayName.length - ircPattern.length)
val ircPattern = " (IRC)"
if (displayName.endsWith(ircPattern)) {
return displayName.substring(0, displayName.length - ircPattern.length)
}
} }
return displayName return displayName
} }
private const val ircPattern = " (IRC)"
} }
@ -255,6 +240,10 @@ class RoomDetailFragment :
} }
} }
roomDetailViewModel.fileTooBigEvent.observeEvent(this) {
displayFileTooBigWarning(it)
}
roomDetailViewModel.selectSubscribe(this, RoomDetailViewState::tombstoneEventHandling, uniqueOnly("tombstoneEventHandling")) { roomDetailViewModel.selectSubscribe(this, RoomDetailViewState::tombstoneEventHandling, uniqueOnly("tombstoneEventHandling")) {
renderTombstoneEventHandling(it) renderTombstoneEventHandling(it)
} }
@ -305,6 +294,18 @@ class RoomDetailFragment :
jumpToReadMarkerView.callback = this jumpToReadMarkerView.callback = this
} }
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() { private fun setupNotificationView() {
notificationAreaView.delegate = object : NotificationAreaView.Delegate { notificationAreaView.delegate = object : NotificationAreaView.Delegate {
@ -1089,23 +1090,23 @@ class RoomDetailFragment :
// var vibrate = false // var vibrate = false
val myDisplayName = session.getUser(session.myUserId)?.displayName val myDisplayName = session.getUser(session.myUserId)?.displayName
if (TextUtils.equals(myDisplayName, text)) { if (myDisplayName == text) {
// current user // current user
if (TextUtils.isEmpty(composerLayout.composerEditText.text)) { if (composerLayout.composerEditText.text.isBlank()) {
composerLayout.composerEditText.append(Command.EMOTE.command + " ") composerLayout.composerEditText.append(Command.EMOTE.command + " ")
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length) composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
// vibrate = true // vibrate = true
} }
} else { } else {
// another user // another user
if (TextUtils.isEmpty(composerLayout.composerEditText.text)) { if (composerLayout.composerEditText.text.isBlank()) {
// Ensure displayName will not be interpreted as a Slash command // Ensure displayName will not be interpreted as a Slash command
if (text.startsWith("/")) { if (text.startsWith("/")) {
composerLayout.composerEditText.append("\\") composerLayout.composerEditText.append("\\")
} }
composerLayout.composerEditText.append(sanitizeDisplayname(text)!! + ": ") composerLayout.composerEditText.append(sanitizeDisplayName(text) + ": ")
} else { } else {
composerLayout.composerEditText.text.insert(composerLayout.composerEditText.selectionStart, sanitizeDisplayname(text)!! + " ") composerLayout.composerEditText.text.insert(composerLayout.composerEditText.selectionStart, sanitizeDisplayName(text) + " ")
} }
// vibrate = true // vibrate = true
@ -1156,5 +1157,4 @@ class RoomDetailFragment :
roomDetailViewModel.process(RoomDetailActions.MarkAllAsRead) roomDetailViewModel.process(RoomDetailActions.MarkAllAsRead)
} }
} }

View File

@ -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.isTextMessage
import im.vector.matrix.android.api.session.events.model.toModel 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.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.Membership
import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.MessageType
@ -49,6 +50,7 @@ import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.riotx.BuildConfig
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.postLiveEvent import im.vector.riotx.core.extensions.postLiveEvent
import im.vector.riotx.core.intent.getFilenameFromUri import im.vector.riotx.core.intent.getFilenameFromUri
@ -239,23 +241,22 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
val navigateToEvent: LiveData<LiveEvent<String>> val navigateToEvent: LiveData<LiveEvent<String>>
get() = _navigateToEvent get() = _navigateToEvent
private val _fileTooBigEvent = MutableLiveData<LiveEvent<FileTooBigError>>()
val fileTooBigEvent: LiveData<LiveEvent<FileTooBigError>>
get() = _fileTooBigEvent
private val _downloadedFileEvent = MutableLiveData<LiveEvent<DownloadFileState>>() private val _downloadedFileEvent = MutableLiveData<LiveEvent<DownloadFileState>>()
val downloadedFileEvent: LiveData<LiveEvent<DownloadFileState>> val downloadedFileEvent: LiveData<LiveEvent<DownloadFileState>>
get() = _downloadedFileEvent get() = _downloadedFileEvent
fun isMenuItemVisible(@IdRes itemId: Int): Boolean { fun isMenuItemVisible(@IdRes itemId: Int) = when (itemId) {
if (itemId == R.id.clear_message_queue) { R.id.clear_message_queue ->
//For now always disable, woker cancellation is not working properly /* For now always disable on production, worker cancellation is not working properly */
return false//timeline.pendingEventCount() > 0 timeline.pendingEventCount() > 0 && BuildConfig.DEBUG
} R.id.resend_all -> timeline.failedToDeliverEventCount() > 0
if (itemId == R.id.resend_all) { R.id.clear_all -> timeline.failedToDeliverEventCount() > 0
return timeline.failedToDeliverEventCount() > 0 else -> false
}
if (itemId == R.id.clear_all) {
return timeline.failedToDeliverEventCount() > 0
}
return false
} }
// PRIVATE METHODS ***************************************************************************** // PRIVATE METHODS *****************************************************************************
@ -482,7 +483,20 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
type = ContentAttachmentData.Type.values()[it.mediaType] type = ContentAttachmentData.Type.values()[it.mediaType]
) )
} }
room.sendMedias(attachments)
val homeServerCapabilities = session.getHomeServerCapabilities()
val maxUploadFileSize = homeServerCapabilities.maxUploadFileSize
if (maxUploadFileSize == HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN) {
// Unknown limitation
room.sendMedias(attachments)
} else {
when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) {
null -> room.sendMedias(attachments)
else -> _fileTooBigEvent.postValue(LiveEvent(FileTooBigError(tooBigFile.name ?: tooBigFile.path, tooBigFile.size, maxUploadFileSize)))
}
}
} }
private fun handleEventVisible(action: RoomDetailActions.TimelineEventTurnsVisible) { private fun handleEventVisible(action: RoomDetailActions.TimelineEventTurnsVisible) {

View File

@ -38,6 +38,8 @@ import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineEventDi
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
import im.vector.riotx.features.home.room.detail.timeline.helper.nextOrNull import im.vector.riotx.features.home.room.detail.timeline.helper.nextOrNull
import im.vector.riotx.core.utils.DimensionConverter
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.item.* import im.vector.riotx.features.home.room.detail.timeline.item.*
import im.vector.riotx.features.media.ImageContentRenderer import im.vector.riotx.features.media.ImageContentRenderer
import im.vector.riotx.features.media.VideoContentRenderer import im.vector.riotx.features.media.VideoContentRenderer
@ -48,6 +50,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
private val timelineItemFactory: TimelineItemFactory, private val timelineItemFactory: TimelineItemFactory,
private val timelineMediaSizeProvider: TimelineMediaSizeProvider, private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
private val mergedHeaderItemFactory: MergedHeaderItemFactory, private val mergedHeaderItemFactory: MergedHeaderItemFactory,
private val avatarRenderer: AvatarRenderer,
private val dimensionConverter: DimensionConverter,
@TimelineEventControllerHandler @TimelineEventControllerHandler
private val backgroundHandler: Handler private val backgroundHandler: Handler
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor { ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor {

View File

@ -47,6 +47,10 @@ import im.vector.riotx.core.linkify.VectorLinkify
import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.utils.DebouncedClickListener 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.TimelineEventController
import im.vector.riotx.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder import im.vector.riotx.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider
@ -131,6 +135,8 @@ class MessageItemFactory @Inject constructor(
attributes: AbsMessageItem.Attributes): MessageFileItem? { attributes: AbsMessageItem.Attributes): MessageFileItem? {
return MessageFileItem_() return MessageFileItem_()
.attributes(attributes) .attributes(attributes)
.izLocalFile(messageContent.getFileUrl().isLocalFile())
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
.highlighted(highlight) .highlighted(highlight)
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
.filename(messageContent.body) .filename(messageContent.body)
@ -149,6 +155,8 @@ class MessageItemFactory @Inject constructor(
return MessageFileItem_() return MessageFileItem_()
.attributes(attributes) .attributes(attributes)
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
.izLocalFile(messageContent.getFileUrl().isLocalFile())
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
.highlighted(highlight) .highlighted(highlight)
.filename(messageContent.body) .filename(messageContent.body)
.iconRes(R.drawable.filetype_attachment) .iconRes(R.drawable.filetype_attachment)

View File

@ -16,7 +16,6 @@
package im.vector.riotx.features.home.room.detail.timeline.helper package im.vector.riotx.features.home.room.detail.timeline.helper
import android.text.format.Formatter
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ProgressBar 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.matrix.android.api.session.room.send.SendState
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder 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.resources.ColorProvider
import im.vector.riotx.features.media.ImageContentRenderer import im.vector.riotx.core.utils.TextUtils
import im.vector.riotx.features.ui.getMessageTextColor import im.vector.riotx.features.ui.getMessageTextColor
import javax.inject.Inject import javax.inject.Inject
class ContentUploadStateTrackerBinder @Inject constructor(private val activeSessionHolder: ActiveSessionHolder, 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>() private val updateListeners = mutableMapOf<String, ContentUploadStateTracker.UpdateListener>()
fun bind(eventId: String, fun bind(eventId: String,
mediaData: ImageContentRenderer.Data, isLocalFile: Boolean,
progressLayout: ViewGroup) { progressLayout: ViewGroup) {
activeSessionHolder.getActiveSession().also { session -> activeSessionHolder.getActiveSession().also { session ->
val uploadStateTracker = session.contentUploadProgressTracker() val uploadStateTracker = session.contentUploadProgressTracker()
val updateListener = ContentMediaProgressUpdater(progressLayout, mediaData, colorProvider) val updateListener = ContentMediaProgressUpdater(progressLayout, isLocalFile, colorProvider, errorFormatter)
updateListeners[eventId] = updateListener updateListeners[eventId] = updateListener
uploadStateTracker.track(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 class ContentMediaProgressUpdater(private val progressLayout: ViewGroup,
private val mediaData: ImageContentRenderer.Data, 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) { override fun onUpdate(state: ContentUploadStateTracker.State) {
when (state) { when (state) {
@ -76,7 +78,7 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup,
} }
private fun handleIdle(state: ContentUploadStateTracker.State.Idle) { private fun handleIdle(state: ContentUploadStateTracker.State.Idle) {
if (mediaData.isLocalFile()) { if (isLocalFile) {
progressLayout.isVisible = true progressLayout.isVisible = true
val progressBar = progressLayout.findViewById<ProgressBar>(R.id.mediaProgressBar) val progressBar = progressLayout.findViewById<ProgressBar>(R.id.mediaProgressBar)
val progressTextView = progressLayout.findViewById<TextView>(R.id.mediaProgressTextView) val progressTextView = progressLayout.findViewById<TextView>(R.id.mediaProgressTextView)
@ -124,8 +126,8 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup,
progressBar?.isIndeterminate = false progressBar?.isIndeterminate = false
progressBar?.progress = percent.toInt() progressBar?.progress = percent.toInt()
progressTextView?.text = progressLayout.context.getString(resId, progressTextView?.text = progressLayout.context.getString(resId,
Formatter.formatShortFileSize(progressLayout.context, current), TextUtils.formatFileSize(progressLayout.context, current, true),
Formatter.formatShortFileSize(progressLayout.context, total)) TextUtils.formatFileSize(progressLayout.context, total, true))
progressTextView?.setTextColor(colorProvider.getMessageTextColor(SendState.SENDING)) 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 progressBar = progressLayout.findViewById<ProgressBar>(R.id.mediaProgressBar)
val progressTextView = progressLayout.findViewById<TextView>(R.id.mediaProgressTextView) val progressTextView = progressLayout.findViewById<TextView>(R.id.mediaProgressTextView)
progressBar?.isVisible = false progressBar?.isVisible = false
progressTextView?.text = state.throwable.localizedMessage progressTextView?.text = errorFormatter.toHumanReadable(state.throwable)
progressTextView?.setTextColor(colorProvider.getMessageTextColor(SendState.UNDELIVERED)) progressTextView?.setTextColor(colorProvider.getMessageTextColor(SendState.UNDELIVERED))
} }

View File

@ -116,6 +116,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
if (!shouldShowReactionAtBottom() || attributes.informationData.orderedReactionList.isNullOrEmpty()) { if (!shouldShowReactionAtBottom() || attributes.informationData.orderedReactionList.isNullOrEmpty()) {
holder.reactionWrapper?.isVisible = false holder.reactionWrapper?.isVisible = false
} else { } else {
//inflate if needed //inflate if needed
if (holder.reactionFlowHelper == null) { if (holder.reactionFlowHelper == null) {

View File

@ -56,7 +56,7 @@ abstract class DefaultItem : BaseEventItem<DefaultItem.Holder>() {
} }
override fun getEventIds(): List<String> { override fun getEventIds(): List<String> {
return informationData.eventId return listOf(informationData.eventId)
} }
override fun getViewType() = STUB_ID override fun getViewType() = STUB_ID

View File

@ -22,9 +22,11 @@ import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
@EpoxyModelClass(layout = R.layout.item_timeline_event_base) @EpoxyModelClass(layout = R.layout.item_timeline_event_base)
abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() { abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
@ -36,19 +38,34 @@ abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
var iconRes: Int = 0 var iconRes: Int = 0
@EpoxyAttribute @EpoxyAttribute
var clickListener: View.OnClickListener? = null var clickListener: View.OnClickListener? = null
@EpoxyAttribute
var izLocalFile = false
@EpoxyAttribute
lateinit var contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
renderSendState(holder.fileLayout, holder.filenameView) renderSendState(holder.fileLayout, holder.filenameView)
if (!attributes.informationData.sendState.hasFailed()) {
contentUploadStateTrackerBinder.bind(attributes.informationData.eventId, izLocalFile, holder.progressLayout)
} else {
holder.progressLayout.isVisible = false
}
holder.filenameView.text = filename holder.filenameView.text = filename
holder.fileImageView.setImageResource(iconRes) holder.fileImageView.setImageResource(iconRes)
holder.filenameView.setOnClickListener(clickListener) holder.filenameView.setOnClickListener(clickListener)
holder.filenameView.paintFlags = (holder.filenameView.paintFlags or Paint.UNDERLINE_TEXT_FLAG) holder.filenameView.paintFlags = (holder.filenameView.paintFlags or Paint.UNDERLINE_TEXT_FLAG)
} }
override fun unbind(holder: Holder) {
super.unbind(holder)
contentUploadStateTrackerBinder.unbind(attributes.informationData.eventId)
}
override fun getViewType() = STUB_ID override fun getViewType() = STUB_ID
class Holder : AbsMessageItem.Holder(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 fileLayout by bind<ViewGroup>(R.id.messageFileLayout)
val fileImageView by bind<ImageView>(R.id.messageFileImageView) val fileImageView by bind<ImageView>(R.id.messageFileImageView)
val filenameView by bind<TextView>(R.id.messageFilenameView) val filenameView by bind<TextView>(R.id.messageFilenameView)

View File

@ -20,6 +20,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R import im.vector.riotx.R
@ -44,11 +45,13 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
super.bind(holder) super.bind(holder)
imageContentRenderer.render(mediaData, ImageContentRenderer.Mode.THUMBNAIL, holder.imageView) imageContentRenderer.render(mediaData, ImageContentRenderer.Mode.THUMBNAIL, holder.imageView)
if (!attributes.informationData.sendState.hasFailed()) { if (!attributes.informationData.sendState.hasFailed()) {
contentUploadStateTrackerBinder.bind(attributes.informationData.eventId, mediaData, holder.progressLayout) contentUploadStateTrackerBinder.bind(attributes.informationData.eventId, mediaData.isLocalFile(), holder.progressLayout)
} else {
holder.progressLayout.isVisible = false
} }
holder.imageView.setOnClickListener(clickListener) holder.imageView.setOnClickListener(clickListener)
holder.imageView.setOnLongClickListener(attributes.itemLongClickListener) holder.imageView.setOnLongClickListener(attributes.itemLongClickListener)
ViewCompat.setTransitionName(holder.imageView,"imagePreview_${id()}") ViewCompat.setTransitionName(holder.imageView, "imagePreview_${id()}")
holder.mediaContentView.setOnClickListener(attributes.itemClickListener) holder.mediaContentView.setOnClickListener(attributes.itemClickListener)
holder.mediaContentView.setOnLongClickListener(attributes.itemLongClickListener) holder.mediaContentView.setOnLongClickListener(attributes.itemLongClickListener)
// The sending state color will be apply to the progress text // The sending state color will be apply to the progress text

View File

@ -33,9 +33,9 @@ import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.glide.GlideApp
import im.vector.riotx.core.glide.GlideRequest import im.vector.riotx.core.glide.GlideRequest
import im.vector.riotx.core.utils.DimensionConverter import im.vector.riotx.core.utils.DimensionConverter
import im.vector.riotx.core.utils.isLocalFile
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import timber.log.Timber import timber.log.Timber
import java.io.File
import javax.inject.Inject import javax.inject.Inject
class ImageContentRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder, class ImageContentRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
@ -54,9 +54,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
val rotation: Int? = null val rotation: Int? = null
) : Parcelable { ) : Parcelable {
fun isLocalFile(): Boolean { fun isLocalFile() = url.isLocalFile()
return url != null && File(url).exists()
}
} }
enum class Mode { enum class Mode {

View File

@ -20,7 +20,7 @@ import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.text.Editable import android.text.Editable
import android.text.TextUtils import android.util.Patterns
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
@ -171,7 +171,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
MXSession.getApplicationSizeCaches(activity, object : SimpleApiCallback<Long>() { MXSession.getApplicationSizeCaches(activity, object : SimpleApiCallback<Long>() {
override fun onSuccess(size: Long) { override fun onSuccess(size: Long) {
if (null != activity) { 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(), val size = getSizeOfFiles(requireContext(),
File(requireContext().cacheDir, DiskCache.Factory.DEFAULT_DISK_CACHE_DIR)) 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 { it.onPreferenceClickListener = Preference.OnPreferenceClickListener {
GlobalScope.launch(Dispatchers.Main) { GlobalScope.launch(Dispatchers.Main) {
@ -208,7 +208,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
File(requireContext().cacheDir, DiskCache.Factory.DEFAULT_DISK_CACHE_DIR)) 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() hideLoadingView()
} }
@ -534,7 +534,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
private fun addEmail(email: String) { private fun addEmail(email: String) {
// check first if the email syntax is valid // check first if the email syntax is valid
// if email is null , then also its invalid email // 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) activity?.toast(R.string.auth_invalid_email)
return return
} }
@ -719,9 +719,9 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
val newPwd = newPasswordText.text.toString().trim() val newPwd = newPasswordText.text.toString().trim()
val newConfirmPwd = confirmNewPasswordText.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) confirmNewPasswordTil.error = getString(R.string.passwords_do_not_match)
} }
} }

View File

@ -0,0 +1,56 @@
/*
* 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.ui
import android.content.SharedPreferences
import androidx.core.content.edit
import im.vector.riotx.features.home.room.list.RoomListFragment
import javax.inject.Inject
/**
* This class is used to persist UI state across application restart
*/
class SharedPreferencesUiStateRepository @Inject constructor(private val sharedPreferences: SharedPreferences) : UiStateRepository {
override fun getDisplayMode(): RoomListFragment.DisplayMode {
return when (sharedPreferences.getInt(KEY_DISPLAY_MODE, VALUE_DISPLAY_MODE_CATCHUP)) {
VALUE_DISPLAY_MODE_PEOPLE -> RoomListFragment.DisplayMode.PEOPLE
VALUE_DISPLAY_MODE_ROOMS -> RoomListFragment.DisplayMode.ROOMS
else -> RoomListFragment.DisplayMode.HOME
}
}
override fun storeDisplayMode(displayMode: RoomListFragment.DisplayMode) {
sharedPreferences.edit {
putInt(KEY_DISPLAY_MODE,
when (displayMode) {
RoomListFragment.DisplayMode.PEOPLE -> VALUE_DISPLAY_MODE_PEOPLE
RoomListFragment.DisplayMode.ROOMS -> VALUE_DISPLAY_MODE_ROOMS
else -> VALUE_DISPLAY_MODE_CATCHUP
})
}
}
companion object {
private const val KEY_DISPLAY_MODE = "UI_STATE_DISPLAY_MODE"
private const val VALUE_DISPLAY_MODE_CATCHUP = 0
private const val VALUE_DISPLAY_MODE_PEOPLE = 1
private const val VALUE_DISPLAY_MODE_ROOMS = 2
}
}

View File

@ -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.riotx.features.ui
import im.vector.riotx.features.home.room.list.RoomListFragment
/**
* This interface is used to persist UI state across application restart
*/
interface UiStateRepository {
fun getDisplayMode(): RoomListFragment.DisplayMode
fun storeDisplayMode(displayMode: RoomListFragment.DisplayMode)
}

View File

@ -2,66 +2,68 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/messageFileLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="wrap_content"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<LinearLayout <ImageView
android:id="@+id/messageFileLayout" android:id="@+id/messageFilee2eIcon"
android:layout_width="14dp"
android:layout_height="14dp"
android:src="@drawable/e2e_verified"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<!-- the media type -->
<ImageView
android:id="@+id/messageFileImageView"
android:layout_width="@dimen/chat_avatar_size"
android:layout_height="@dimen/chat_avatar_size"
android:layout_marginStart="4dp"
android:layout_marginLeft="4dp"
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_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginStart="4dp"
android:layout_marginLeft="4dp"
android:layout_marginEnd="32dp" android:layout_marginEnd="32dp"
android:layout_marginRight="32dp" android:autoLink="none"
android:layout_marginBottom="8dp" android:gravity="center_vertical"
android:duplicateParentState="true" android:minHeight="@dimen/chat_avatar_size"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0" app:layout_constraintStart_toEndOf="@+id/messageFileImageView"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintTop_toTopOf="parent"> tools:text="A filename here" />
<ImageView <androidx.constraintlayout.widget.Barrier
android:id="@+id/messageFilee2eIcon" android:id="@+id/horizontalBarrier"
android:layout_width="14dp" android:layout_width="wrap_content"
android:layout_height="14dp" android:layout_height="wrap_content"
android:src="@drawable/e2e_verified" app:barrierDirection="bottom"
android:visibility="gone" /> app:constraint_referenced_ids="messageFileImageView,messageFilenameView" />
<!-- the media type -->
<ImageView
android:id="@+id/messageFileImageView"
android:layout_width="@dimen/chat_avatar_size"
android:layout_height="@dimen/chat_avatar_size"
android:layout_marginStart="4dp"
android:layout_marginLeft="4dp"
android:src="@drawable/filetype_image" />
<!-- the media -->
<TextView
android:id="@+id/messageFilenameView"
android:layout_width="0dp"
android:layout_height="@dimen/chat_avatar_size"
android:layout_marginStart="4dp"
android:layout_marginLeft="4dp"
android:layout_weight="1"
android:autoLink="none"
android:gravity="center_vertical"
tools:text="A filename here" />
</LinearLayout>
<include <include
android:id="@+id/messageMediaUploadProgressLayout" android:id="@+id/messageFileUploadProgressLayout"
layout="@layout/media_upload_download_progress_layout" layout="@layout/media_upload_download_progress_layout"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="46dp" android:layout_height="46dp"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:layout_marginEnd="32dp" android:layout_marginEnd="32dp"
android:layout_marginRight="32dp" android:layout_marginRight="32dp"
android:layout_marginBottom="8dp"
android:visibility="gone" android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/messageFileLayout" app:layout_constraintTop_toBottomOf="@+id/horizontalBarrier"
tools:visibility="visible" /> tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -22,7 +22,7 @@
<item <item
android:id="@+id/clear_message_queue" android:id="@+id/clear_message_queue"
android:title="@string/clear_timeline_send_queue" android:title="@string/clear_timeline_send_queue"
android:visible="false" android:visible="@bool/debug_mode"
app:showAsAction="never" app:showAsAction="never"
tools:visible="true" /> tools:visible="true" />

View File

@ -22,4 +22,6 @@
<string name="a11y_create_room">Create a new room</string> <string name="a11y_create_room">Create a new room</string>
<string name="a11y_close_keys_backup_banner">Close keys backup banner</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> </resources>