Merge pull request #318 from vector-im/feature/send_state

Fix some bugs on e2e rooms
This commit is contained in:
Benoit Marty 2019-07-09 15:03:39 +02:00 committed by GitHub
commit a0bd206308
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 677 additions and 197 deletions

View File

@ -24,6 +24,7 @@ import im.vector.matrix.android.api.session.cache.CacheService
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.file.FileService
import im.vector.matrix.android.api.session.group.GroupService
import im.vector.matrix.android.api.session.pushers.PushersService
import im.vector.matrix.android.api.session.room.RoomDirectoryService
@ -46,6 +47,7 @@ interface Session :
CacheService,
SignOutService,
FilterService,
FileService,
PushRuleService,
PushersService {


View File

@ -28,10 +28,11 @@ interface ContentUploadStateTracker {

sealed class State {
object Idle : State()
data class ProgressData(val current: Long, val total: Long) : State()
object EncryptingThumbnail : State()
data class UploadingThumbnail(val current: Long, val total: Long) : State()
object Encrypting : State()
data class Uploading(val current: Long, val total: Long) : State()
object Success : State()
object Failure : State()
data class Failure(val throwable: Throwable) : State()
}


}

View File

@ -26,12 +26,14 @@ import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
import im.vector.matrix.android.internal.crypto.NewSessionListener
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
import java.io.File

interface CryptoService {


View File

@ -0,0 +1,52 @@
/*
* 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.file

import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
import java.io.File


/**
* This interface defines methods to get files.
*/
interface FileService {

enum class DownloadMode {
/**
* Download file in external storage
*/
TO_EXPORT,
/**
* Download file in cache
*/
FOR_INTERNAL_USE
}

/**
* Download a file.
* Result will be a decrypted file, stored in the cache folder. id parameter will be used to create a sub folder to avoid name collision.
* You can pass the eventId
*/
fun downloadFile(
downloadMode: DownloadMode,
id: String,
fileName: String,
url: String?,
elementToDecrypt: ElementToDecrypt?,
callback: MatrixCallback<File>)
}

View File

@ -42,7 +42,7 @@ data class MessageAudioContent(
/**
* Required. Required if the file is not encrypted. The URL (typically MXC URI) to the audio clip.
*/
@Json(name = "url") val url: String? = null,
@Json(name = "url") override val url: String? = null,

@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null,
@ -51,4 +51,4 @@ data class MessageAudioContent(
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
*/
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
) : MessageEncyptedContent
) : MessageEncryptedContent

View File

@ -20,8 +20,18 @@ import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo


/**
* Interface for message which can contains encrypted data
* Interface for message which can contains an encrypted file
*/
interface MessageEncyptedContent : MessageContent {
interface MessageEncryptedContent : MessageContent {
/**
* Required. Required if the file is unencrypted. The URL (typically MXC URI) to the image.
*/
val url: String?

val encryptedFileInfo: EncryptedFileInfo?
}

/**
* Get the url of the encrypted file or of the file
*/
fun MessageEncryptedContent.getFileUrl() = encryptedFileInfo?.url ?: url

View File

@ -16,6 +16,7 @@

package im.vector.matrix.android.api.session.room.model.message

import android.content.ClipDescription
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.events.model.Content
@ -47,10 +48,22 @@ data class MessageFileContent(
/**
* Required. Required if the file is unencrypted. The URL (typically MXC URI) to the file.
*/
@Json(name = "url") val url: String? = null,
@Json(name = "url") override val url: String? = null,

@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null,

@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
) : MessageEncyptedContent
) : MessageEncryptedContent {

fun getMimeType(): String {
// Mimetype default to plain text, should not be used
return encryptedFileInfo?.mimetype
?: info?.mimeType
?: ClipDescription.MIMETYPE_TEXT_PLAIN
}

fun getFileName(): String {
return filename ?: body
}
}

View File

@ -43,7 +43,7 @@ data class MessageImageContent(
/**
* Required. Required if the file is unencrypted. The URL (typically MXC URI) to the image.
*/
@Json(name = "url") val url: String? = null,
@Json(name = "url") override val url: String? = null,

@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null,
@ -52,4 +52,4 @@ data class MessageImageContent(
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
*/
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
) : MessageEncyptedContent
) : MessageEncryptedContent

View File

@ -42,7 +42,7 @@ data class MessageVideoContent(
/**
* Required. Required if the file is unencrypted. The URL (typically MXC URI) to the video clip.
*/
@Json(name = "url") val url: String? = null,
@Json(name = "url") override val url: String? = null,

@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null,
@ -51,4 +51,4 @@ data class MessageVideoContent(
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
*/
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
) : MessageEncyptedContent
) : MessageEncryptedContent

View File

@ -48,6 +48,7 @@ import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAct
import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting
import im.vector.matrix.android.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory
import im.vector.matrix.android.internal.crypto.algorithms.olm.MXOlmEncryptionFactory
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
@ -78,6 +79,7 @@ import im.vector.matrix.android.internal.util.fetchCopied
import kotlinx.coroutines.*
import org.matrix.olm.OlmManager
import timber.log.Timber
import java.io.File
import java.util.*
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject

View File

@ -16,8 +16,8 @@

package im.vector.matrix.android.internal.crypto.attachments

import android.text.TextUtils
import android.util.Base64
import arrow.core.Try
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileKey
import timber.log.Timber
@ -51,7 +51,7 @@ object MXEncryptedAttachments {
* @param mimetype the mime type
* @return the encryption file info
*/
fun encryptAttachment(attachmentStream: InputStream, mimetype: String): EncryptionResult? {
fun encryptAttachment(attachmentStream: InputStream, mimetype: String): Try<EncryptionResult> {
val t0 = System.currentTimeMillis()
val secureRandom = SecureRandom()

@ -115,23 +115,21 @@ object MXEncryptedAttachments {
encryptedByteArray = outStream.toByteArray()
)

outStream.close()

Timber.v("Encrypt in " + (System.currentTimeMillis() - t0) + " ms")
return result
return Try.just(result)
} catch (oom: OutOfMemoryError) {
Timber.e(oom, "## encryptAttachment failed " + oom.message)
Timber.e(oom, "## encryptAttachment failed")
return Try.Failure(oom)
} catch (e: Exception) {
Timber.e(e, "## encryptAttachment failed " + e.message)
}

Timber.e(e, "## encryptAttachment failed")
return Try.Failure(e)
} finally {
try {
outStream.close()
} catch (e: Exception) {
Timber.e(e, "## encryptAttachment() : fail to close outStream")
}

return null
}
}

/**
@ -199,7 +197,7 @@ object MXEncryptedAttachments {

val currentDigestValue = base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))

if (!TextUtils.equals(elementToDecrypt.sha256, currentDigestValue)) {
if (elementToDecrypt.sha256 != currentDigestValue) {
Timber.e("## decryptAttachment() : Digest value mismatch")
outStream.close()
return null

View File

@ -0,0 +1,121 @@
/*
* 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

import android.content.Context
import android.os.Environment
import arrow.core.Try
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.matrix.android.api.session.file.FileService
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments
import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.md5
import im.vector.matrix.android.internal.util.writeToFile
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import timber.log.Timber
import java.io.File
import java.io.IOException
import javax.inject.Inject

internal class DefaultFileService @Inject constructor(private val context: Context,
private val sessionParams: SessionParams,
private val contentUrlResolver: ContentUrlResolver,
private val coroutineDispatchers: MatrixCoroutineDispatchers) : FileService {

val okHttpClient = OkHttpClient()

/**
* Download file in the cache folder, and eventually decrypt it
* TODO implement clear file, to delete "MF"
*/
override fun downloadFile(downloadMode: FileService.DownloadMode,
id: String,
fileName: String,
url: String?,
elementToDecrypt: ElementToDecrypt?,
callback: MatrixCallback<File>) {
GlobalScope.launch(coroutineDispatchers.main) {
withContext(coroutineDispatchers.io) {
Try {
val folder = getFolder(downloadMode, id)

File(folder, fileName)
}.flatMap { destFile ->
if (!destFile.exists() || downloadMode == FileService.DownloadMode.TO_EXPORT) {
Try {
val resolvedUrl = contentUrlResolver.resolveFullSize(url) ?: throw IllegalArgumentException("url is null")

val request = Request.Builder()
.url(resolvedUrl)
.build()

val response = okHttpClient.newCall(request).execute()
val inputStream = response.body()?.byteStream()
Timber.v("Response size ${response.body()?.contentLength()} - Stream available: ${inputStream?.available()}")
if (!response.isSuccessful
|| inputStream == null) {
throw IOException()
}

if (elementToDecrypt != null) {
Timber.v("## decrypt file")
MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt) ?: throw IllegalStateException("Decryption error")
} else {
inputStream
}
}
.map { inputStream ->
writeToFile(inputStream, destFile)
destFile
}
} else {
Try.just(destFile)
}
}
}
.foldToCallback(callback)
}
}

private fun getFolder(downloadMode: FileService.DownloadMode, id: String): File {
return when (downloadMode) {
FileService.DownloadMode.FOR_INTERNAL_USE -> {
// Create dir tree (MF stands for Matrix File):
// <cache>/MF/<md5(userId)>/<md5(id)>/
val tmpFolderRoot = File(context.cacheDir, "MF")
val tmpFolderUser = File(tmpFolderRoot, sessionParams.credentials.userId.md5())
File(tmpFolderUser, id.md5())
}
FileService.DownloadMode.TO_EXPORT -> {
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
}
}
.also { folder ->
if (!folder.exists()) {
folder.mkdirs()
}
}
}
}

View File

@ -21,7 +21,6 @@ import android.os.Looper
import androidx.annotation.MainThread
import androidx.lifecycle.LiveData
import androidx.work.WorkManager
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.pushrules.PushRuleService
@ -30,6 +29,7 @@ import im.vector.matrix.android.api.session.cache.CacheService
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.file.FileService
import im.vector.matrix.android.api.session.group.GroupService
import im.vector.matrix.android.api.session.pushers.PushersService
import im.vector.matrix.android.api.session.room.RoomDirectoryService
@ -61,6 +61,7 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
private val pushRuleService: PushRuleService,
private val pushersService: PushersService,
private val cryptoService: CryptoManager,
private val fileService: FileService,
private val syncThread: SyncThread,
private val contentUrlResolver: ContentUrlResolver,
private val contentUploadProgressTracker: ContentUploadStateTracker)
@ -73,6 +74,7 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
CacheService by cacheService,
SignOutService by signOutService,
FilterService by filterService,
FileService by fileService,
PushRuleService by pushRuleService,
PushersService by pushersService {


View File

@ -43,8 +43,8 @@ internal class DefaultContentUploadStateTracker @Inject constructor() : ContentU
}
}

internal fun setFailure(key: String) {
val failure = ContentUploadStateTracker.State.Failure
internal fun setFailure(key: String, throwable: Throwable) {
val failure = ContentUploadStateTracker.State.Failure(throwable)
updateState(key, failure)
}

@ -53,8 +53,23 @@ internal class DefaultContentUploadStateTracker @Inject constructor() : ContentU
updateState(key, success)
}

internal fun setEncryptingThumbnail(key: String) {
val progressData = ContentUploadStateTracker.State.EncryptingThumbnail
updateState(key, progressData)
}

internal fun setProgressThumbnail(key: String, current: Long, total: Long) {
val progressData = ContentUploadStateTracker.State.UploadingThumbnail(current, total)
updateState(key, progressData)
}

internal fun setEncrypting(key: String) {
val progressData = ContentUploadStateTracker.State.Encrypting
updateState(key, progressData)
}

internal fun setProgress(key: String, current: Long, total: Long) {
val progressData = ContentUploadStateTracker.State.ProgressData(current, total)
val progressData = ContentUploadStateTracker.State.Uploading(current, total)
updateState(key, progressData)
}


View File

@ -48,7 +48,7 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
val event: Event,
val attachment: ContentAttachmentData,
val isRoomEncrypted: Boolean,
override var lastFailureMessage: String? = null
override val lastFailureMessage: String? = null
) : SessionWorkerParams

@Inject lateinit var fileUploader: FileUploader
@ -69,27 +69,47 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
val eventId = params.event.eventId ?: return Result.success()
val attachment = params.attachment

val isRoomEncrypted = params.isRoomEncrypted
val attachmentFile = try {
File(attachment.path)
} catch (e: Exception) {
Timber.e(e)
contentUploadStateTracker.setFailure(params.event.eventId, e)
return Result.success(
WorkerParamsFactory.toData(params.copy(
lastFailureMessage = e.localizedMessage
))
)
}


val thumbnailData = ThumbnailExtractor.extractThumbnail(params.attachment)
val attachmentFile = createAttachmentFile(attachment) ?: return Result.failure()
var uploadedThumbnailUrl: String? = null
var uploadedThumbnailEncryptedFileInfo: EncryptedFileInfo? = null

if (thumbnailData != null) {
val contentUploadResponse = if (isRoomEncrypted) {
Timber.v("Encrypt thumbnail")
val encryptionResult = MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType)
?: return Result.failure()
ThumbnailExtractor.extractThumbnail(params.attachment)?.let { thumbnailData ->
val thumbnailProgressListener = object : ProgressRequestBody.Listener {
override fun onProgress(current: Long, total: Long) {
contentUploadStateTracker.setProgressThumbnail(eventId, current, total)
}
}

val contentUploadResponse = if (params.isRoomEncrypted) {
Timber.v("Encrypt thumbnail")
contentUploadStateTracker.setEncryptingThumbnail(eventId)
MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType)
.flatMap { encryptionResult ->
uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo

fileUploader
.uploadByteArray(encryptionResult.encryptedByteArray, "thumb_${attachment.name}", thumbnailData.mimeType)
.uploadByteArray(encryptionResult.encryptedByteArray,
"thumb_${attachment.name}",
"application/octet-stream",
thumbnailProgressListener)
}
} else {
fileUploader
.uploadByteArray(thumbnailData.bytes, "thumb_${attachment.name}", thumbnailData.mimeType)
.uploadByteArray(thumbnailData.bytes,
"thumb_${attachment.name}",
thumbnailData.mimeType,
thumbnailProgressListener)
}

contentUploadResponse
@ -107,16 +127,17 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :

var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null

val contentUploadResponse = if (isRoomEncrypted) {
val contentUploadResponse = if (params.isRoomEncrypted) {
Timber.v("Encrypt file")
contentUploadStateTracker.setEncrypting(eventId)

val encryptionResult = MXEncryptedAttachments.encryptAttachment(FileInputStream(attachmentFile), attachment.mimeType)
?: return Result.failure()

MXEncryptedAttachments.encryptAttachment(FileInputStream(attachmentFile), attachment.mimeType)
.flatMap { encryptionResult ->
uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo

fileUploader
.uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener)
}
} else {
fileUploader
.uploadFile(attachmentFile, attachment.name, attachment.mimeType, progressListener)
@ -129,17 +150,8 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
)
}

private fun createAttachmentFile(attachment: ContentAttachmentData): File? {
return try {
File(attachment.path)
} catch (e: Exception) {
Timber.e(e)
null
}
}

private fun handleFailure(params: Params, failure: Throwable): Result {
contentUploadStateTracker.setFailure(params.event.eventId!!)
contentUploadStateTracker.setFailure(params.event.eventId!!, failure)
return Result.success(
WorkerParamsFactory.toData(
params.copy(
@ -190,9 +202,10 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
thumbnailEncryptedFileInfo: EncryptedFileInfo?): MessageVideoContent {
return copy(
url = if (encryptedFileInfo == null) url else null,
encryptedFileInfo = encryptedFileInfo?.copy(url = url),
videoInfo = videoInfo?.copy(
thumbnailUrl = if (thumbnailEncryptedFileInfo == null) thumbnailUrl else null,
thumbnailFile = thumbnailEncryptedFileInfo?.copy(url = url)
thumbnailFile = thumbnailEncryptedFileInfo?.copy(url = thumbnailUrl)
)
)
}

View File

@ -32,7 +32,7 @@ internal class GetGroupDataWorker(context: Context, params: WorkerParameters) :
internal data class Params(
override val userId: String,
val groupIds: List<String>,
override var lastFailureMessage: String? = null
override val lastFailureMessage: String? = null
) : SessionWorkerParams

@Inject lateinit var getGroupDataTask: GetGroupDataTask

View File

@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.session.room
import dagger.Binds
import dagger.Module
import dagger.Provides
import im.vector.matrix.android.api.session.file.FileService
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.members.MembershipService
@ -27,6 +28,7 @@ import im.vector.matrix.android.api.session.room.read.ReadService
import im.vector.matrix.android.api.session.room.send.SendService
import im.vector.matrix.android.api.session.room.state.StateService
import im.vector.matrix.android.api.session.room.timeline.TimelineService
import im.vector.matrix.android.internal.session.DefaultFileService
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.room.create.CreateRoomTask
import im.vector.matrix.android.internal.session.room.create.DefaultCreateRoomTask
@ -138,4 +140,6 @@ internal abstract class RoomModule {
@Binds
abstract fun bindTimelineService(timelineService: DefaultTimelineService): TimelineService

@Binds
abstract fun bindFileService(fileService: DefaultFileService): FileService
}

View File

@ -40,7 +40,7 @@ internal class SendRelationWorker(context: Context, params: WorkerParameters) :
val roomId: String,
val event: Event,
val relationType: String? = null,
override var lastFailureMessage: String?
override val lastFailureMessage: String?
) : SessionWorkerParams

@Inject lateinit var roomAPI: RoomAPI

View File

@ -42,7 +42,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
val event: Event,
/**Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to)*/
val keepKeys: List<String>? = null,
override var lastFailureMessage: String? = null
override val lastFailureMessage: String? = null
) : SessionWorkerParams

@Inject lateinit var crypto: CryptoService

View File

@ -36,7 +36,7 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters) : C
val roomId: String,
val eventId: String,
val reason: String?,
override var lastFailureMessage: String? = null
override val lastFailureMessage: String? = null
) : SessionWorkerParams

@Inject lateinit var roomAPI: RoomAPI

View File

@ -39,7 +39,7 @@ internal class SendEventWorker constructor(context: Context, params: WorkerParam
override val userId: String,
val roomId: String,
val event: Event,
override var lastFailureMessage: String? = null
override val lastFailureMessage: String? = null
) : SessionWorkerParams

@Inject lateinit var localEchoUpdater: LocalEchoUpdater

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.util

import androidx.annotation.WorkerThread
import okio.Okio
import java.io.File
import java.io.InputStream

/**
* Save an input stream to a file with Okio
*/
@WorkerThread
fun writeToFile(inputStream: InputStream, outputFile: File) {
val source = Okio.buffer(Okio.source(inputStream))
val sink = Okio.buffer(Okio.sink(outputFile))

source.use { input ->
sink.use { output ->
output.writeAll(input)
}
}
}

View File

@ -20,5 +20,5 @@ interface SessionWorkerParams {
val userId: String

// Null is no error occurs. When chaining Workers, first step is to check that there is no lastFailureMessage from the previous workers
var lastFailureMessage: String?
val lastFailureMessage: String?
}

View File

@ -23,6 +23,7 @@ import arrow.core.Try
import okio.Okio
import timber.log.Timber
import java.io.File
import java.io.InputStream

/**
* Save a string to a file with Okio

View File

@ -66,6 +66,7 @@ const val PERMISSION_REQUEST_CODE_AUDIO_CALL = 571
const val PERMISSION_REQUEST_CODE_VIDEO_CALL = 572
const val PERMISSION_REQUEST_CODE_EXPORT_KEYS = 573
const val PERMISSION_REQUEST_CODE_CHANGE_AVATAR = 574
const val PERMISSION_REQUEST_CODE_DOWNLOAD_FILE = 575

/**
* Log the used permissions statuses.

View File

@ -27,7 +27,7 @@ import im.vector.riotx.R
import im.vector.riotx.core.dialogs.ExportKeysDialog
import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.platform.SimpleFragmentActivity
import im.vector.riotx.core.utils.toast
import im.vector.riotx.core.utils.*
import im.vector.riotx.features.crypto.keys.KeysExporter

class KeysBackupSetupActivity : SimpleFragmentActivity() {
@ -132,6 +132,7 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
}

private fun exportKeysManually() {
if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_EXPORT_KEYS)) {
ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener {
override fun onPassphrase(passphrase: String) {
showWaitingView()
@ -164,7 +165,15 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
}
})
}
}

override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
if (allGranted(grantResults)) {
if (requestCode == PERMISSION_REQUEST_CODE_EXPORT_KEYS) {
exportKeysManually()
}
}
}

override fun onBackPressed() {
if (viewModel.shouldPromptOnBack) {

View File

@ -18,6 +18,7 @@ package im.vector.riotx.features.home.room.detail

import com.jaiselrahman.filepicker.model.MediaFile
import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent

@ -33,6 +34,7 @@ sealed class RoomDetailActions {
data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val add: Boolean) : RoomDetailActions()
data class ShowEditHistoryAction(val event: String, val editAggregatedSummary: EditAggregatedSummary) : RoomDetailActions()
data class NavigateToEvent(val eventId: String, val position: Int?) : RoomDetailActions()
data class DownloadFile(val eventId: String, val messageFileContent: MessageFileContent) : RoomDetailActions()
object AcceptInvite : RoomDetailActions()
object RejectInvite : RoomDetailActions()


View File

@ -63,19 +63,14 @@ import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.dialogs.DialogListItem
import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.extensions.hideKeyboard
import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.core.files.addEntryToDownloadManager
import im.vector.riotx.core.glide.GlideApp
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
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.checkPermissions
import im.vector.riotx.core.utils.copyToClipboard
import im.vector.riotx.core.utils.openCamera
import im.vector.riotx.core.utils.shareMedia
import im.vector.riotx.core.utils.*
import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter
import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy
import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter
@ -180,6 +175,7 @@ class RoomDetailFragment :
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager
@Inject lateinit var roomDetailViewModelFactory: RoomDetailViewModel.Factory
@Inject lateinit var textComposerViewModelFactory: TextComposerViewModel.Factory
@Inject lateinit var errorFormatter: ErrorFormatter
private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
private lateinit var scrollOnHighlightedEventCallback: ScrollOnHighlightedEventCallback

@ -220,6 +216,15 @@ class RoomDetailFragment :
scrollOnHighlightedEventCallback.scheduleScrollTo(it)
}

roomDetailViewModel.downloadedFileEvent.observeEvent(this) { downloadFileState ->
if (downloadFileState.throwable != null) {
requireActivity().toast(errorFormatter.toHumanReadable(downloadFileState.throwable))
} else if (downloadFileState.file != null) {
requireActivity().toast(getString(R.string.downloaded_file, downloadFileState.file.path))
addEntryToDownloadManager(requireContext(), downloadFileState.file, downloadFileState.mimeType)
}
}

roomDetailViewModel.selectSubscribe(
RoomDetailViewState::sendMode,
RoomDetailViewState::selectedEvent,
@ -615,8 +620,27 @@ class RoomDetailFragment :
startActivity(intent)
}

override fun onFileMessageClicked(messageFileContent: MessageFileContent) {
vectorBaseActivity.notImplemented("open file")
override fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) {
val action = RoomDetailActions.DownloadFile(eventId, messageFileContent)
// We need WRITE_EXTERNAL permission
if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_DOWNLOAD_FILE)) {
roomDetailViewModel.process(action)
} else {
roomDetailViewModel.pendingAction = action
}
}

override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
if (allGranted(grantResults)) {
if (requestCode == PERMISSION_REQUEST_CODE_DOWNLOAD_FILE) {
val action = roomDetailViewModel.pendingAction

if (action != null) {
roomDetailViewModel.pendingAction = null
roomDetailViewModel.process(action)
}
}
}
}

override fun onAudioMessageClicked(messageAudioContent: MessageAudioContent) {

View File

@ -31,10 +31,13 @@ import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.content.ContentAttachmentData
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.room.model.Membership
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.model.message.getFileUrl
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
import im.vector.matrix.rx.rx
import im.vector.riotx.R
import im.vector.riotx.core.intent.getFilenameFromUri
@ -50,6 +53,7 @@ import io.reactivex.rxkotlin.subscribeBy
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
import timber.log.Timber
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.TimeUnit
@ -71,6 +75,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
private var timeline = room.createTimeline(eventId, allowedTypes)

// Slot to keep a pending action during permission request
var pendingAction: RoomDetailActions? = null

@AssistedInject.Factory
interface Factory {
fun create(initialState: RoomDetailViewState): RoomDetailViewModel
@ -113,6 +120,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
is RoomDetailActions.EnterEditMode -> handleEditAction(action)
is RoomDetailActions.EnterQuoteMode -> handleQuoteAction(action)
is RoomDetailActions.EnterReplyMode -> handleReplyAction(action)
is RoomDetailActions.DownloadFile -> handleDownloadFile(action)
is RoomDetailActions.NavigateToEvent -> handleNavigateToEvent(action)
else -> Timber.e("Unhandled Action: $action")
}
@ -149,6 +157,10 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
val navigateToEvent: LiveData<LiveEvent<String>>
get() = _navigateToEvent

private val _downloadedFileEvent = MutableLiveData<LiveEvent<DownloadFileState>>()
val downloadedFileEvent: LiveData<LiveEvent<DownloadFileState>>
get() = _downloadedFileEvent


// PRIVATE METHODS *****************************************************************************

@ -433,6 +445,40 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
}

data class DownloadFileState(
val mimeType: String,
val file: File?,
val throwable: Throwable?
)

private fun handleDownloadFile(action: RoomDetailActions.DownloadFile) {
session.downloadFile(
FileService.DownloadMode.TO_EXPORT,
action.eventId,
action.messageFileContent.getFileName(),
action.messageFileContent.getFileUrl(),
action.messageFileContent.encryptedFileInfo?.toElementToDecrypt(),
object : MatrixCallback<File> {
override fun onSuccess(data: File) {
_downloadedFileEvent.postValue(LiveEvent(DownloadFileState(
action.messageFileContent.getMimeType(),
data,
null
)))
}

override fun onFailure(failure: Throwable) {
_downloadedFileEvent.postValue(LiveEvent(DownloadFileState(
action.messageFileContent.getMimeType(),
null,
failure
)))
}
})

}


private fun handleNavigateToEvent(action: RoomDetailActions.NavigateToEvent) {
val targetEventId = action.eventId


View File

@ -57,7 +57,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View)
fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View)
fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View)
fun onFileMessageClicked(messageFileContent: MessageFileContent)
fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent)
fun onAudioMessageClicked(messageAudioContent: MessageAudioContent)
fun onEditedDecorationClicked(informationData: MessageInformationData, editAggregatedSummary: EditAggregatedSummary?)
}

View File

@ -29,14 +29,7 @@ import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan
import im.vector.matrix.android.api.session.events.model.RelationType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary
import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageEmoteContent
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
import im.vector.matrix.android.api.session.room.model.message.MessageNoticeContent
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
@ -169,8 +162,12 @@ class MessageItemFactory @Inject constructor(
}
.clickListener(
DebouncedClickListener(View.OnClickListener { _ ->
callback?.onFileMessageClicked(messageContent)
callback?.onFileMessageClicked(informationData.eventId, messageContent)
}))
.longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false
}
}

private fun buildNotHandledMessageItem(messageContent: MessageContent, highlight: Boolean): DefaultItem? {
@ -188,7 +185,7 @@ class MessageItemFactory @Inject constructor(
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
val data = ImageContentRenderer.Data(
filename = messageContent.body,
url = messageContent.encryptedFileInfo?.url ?: messageContent.url,
url = messageContent.getFileUrl(),
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(),
height = messageContent.info?.height,
maxHeight = maxHeight,
@ -239,8 +236,10 @@ class MessageItemFactory @Inject constructor(
)

val videoData = VideoContentRenderer.Data(
eventId = informationData.eventId,
filename = messageContent.body,
videoUrl = messageContent.url,
url = messageContent.getFileUrl(),
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(),
thumbnailMediaData = thumbnailData
)


View File

@ -16,17 +16,16 @@

package im.vector.riotx.features.home.room.detail.timeline.helper

import android.content.Context
import android.text.format.Formatter
import android.view.View
import android.view.ViewGroup
import android.widget.ProgressBar
import android.widget.TextView
import androidx.core.view.isVisible
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.features.media.ImageContentRenderer
import java.io.File
import javax.inject.Inject

class ContentUploadStateTrackerBinder @Inject constructor(private val activeSessionHolder: ActiveSessionHolder) {
@ -62,44 +61,76 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup,
override fun onUpdate(state: ContentUploadStateTracker.State) {
when (state) {
is ContentUploadStateTracker.State.Idle -> handleIdle(state)
is ContentUploadStateTracker.State.EncryptingThumbnail -> handleEncryptingThumbnail(state)
is ContentUploadStateTracker.State.UploadingThumbnail -> handleProgressThumbnail(state)
is ContentUploadStateTracker.State.Encrypting -> handleEncrypting(state)
is ContentUploadStateTracker.State.Uploading -> handleProgress(state)
is ContentUploadStateTracker.State.Failure -> handleFailure(state)
is ContentUploadStateTracker.State.Success -> handleSuccess(state)
is ContentUploadStateTracker.State.ProgressData -> handleProgress(state)
}
}

private fun handleIdle(state: ContentUploadStateTracker.State.Idle) {
if (mediaData.isLocalFile()) {
val file = File(mediaData.url)
progressLayout.isVisible = true
val progressBar = progressLayout.findViewById<ProgressBar>(R.id.mediaProgressBar)
val progressTextView = progressLayout.findViewById<TextView>(R.id.mediaProgressTextView)
progressBar?.isVisible = true
progressBar?.isIndeterminate = true
progressBar?.progress = 0
progressTextView?.text = progressLayout.context.getString(R.string.send_file_step_idle)
} else {
progressLayout.isVisible = false
}
}

private fun handleEncryptingThumbnail(state: ContentUploadStateTracker.State.EncryptingThumbnail) {
doHandleEncrypting(R.string.send_file_step_encrypting_thumbnail)
}

private fun handleProgressThumbnail(state: ContentUploadStateTracker.State.UploadingThumbnail) {
doHandleProgress(R.string.send_file_step_sending_thumbnail, state.current, state.total)
}

private fun handleEncrypting(state: ContentUploadStateTracker.State.Encrypting) {
doHandleEncrypting(R.string.send_file_step_encrypting_file)
}

private fun handleProgress(state: ContentUploadStateTracker.State.Uploading) {
doHandleProgress(R.string.send_file_step_sending_file, state.current, state.total)
}

private fun doHandleEncrypting(resId: Int) {
progressLayout.visibility = View.VISIBLE
val progressBar = progressLayout.findViewById<ProgressBar>(R.id.mediaProgressBar)
val progressTextView = progressLayout.findViewById<TextView>(R.id.mediaProgressTextView)
progressBar?.progress = 0
progressTextView?.text = formatStats(progressLayout.context, 0L, file.length())
} else {
progressLayout.visibility = View.GONE
progressBar?.isIndeterminate = true
progressTextView?.text = progressLayout.context.getString(resId)
}

private fun doHandleProgress(resId: Int, current: Long, total: Long) {
progressLayout.visibility = View.VISIBLE
val percent = 100L * (current.toFloat() / total.toFloat())
val progressBar = progressLayout.findViewById<ProgressBar>(R.id.mediaProgressBar)
val progressTextView = progressLayout.findViewById<TextView>(R.id.mediaProgressTextView)
progressBar?.isVisible = true
progressBar?.isIndeterminate = false
progressBar?.progress = percent.toInt()
progressTextView?.text = progressLayout.context.getString(resId,
Formatter.formatShortFileSize(progressLayout.context, current),
Formatter.formatShortFileSize(progressLayout.context, total))
}

private fun handleFailure(state: ContentUploadStateTracker.State.Failure) {

progressLayout.visibility = View.VISIBLE
val progressBar = progressLayout.findViewById<ProgressBar>(R.id.mediaProgressBar)
val progressTextView = progressLayout.findViewById<TextView>(R.id.mediaProgressTextView)
progressBar?.isVisible = false
// TODO Red text
progressTextView?.text = state.throwable.localizedMessage
}

private fun handleSuccess(state: ContentUploadStateTracker.State.Success) {

progressLayout.visibility = View.GONE
}

private fun handleProgress(state: ContentUploadStateTracker.State.ProgressData) {
progressLayout.visibility = View.VISIBLE
val percent = 100L * (state.current.toFloat() / state.total.toFloat())
val progressBar = progressLayout.findViewById<ProgressBar>(R.id.mediaProgressBar)
val progressTextView = progressLayout.findViewById<TextView>(R.id.mediaProgressTextView)
progressBar?.progress = percent.toInt()
progressTextView?.text = formatStats(progressLayout.context, state.current, state.total)
}

private fun formatStats(context: Context, current: Long, total: Long): String {
return "${Formatter.formatShortFileSize(context, current)} / ${Formatter.formatShortFileSize(context, total)}"
}

}

View File

@ -100,7 +100,6 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
return
}

// TODO DECRYPT_FILE Decrypt file
imageView.showImage(
Uri.parse(thumbnail),
Uri.parse(fullSize)

View File

@ -18,26 +18,90 @@ package im.vector.riotx.features.media

import android.os.Parcelable
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import android.widget.VideoView
import androidx.core.view.isVisible
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.file.FileService
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.error.ErrorFormatter
import kotlinx.android.parcel.Parcelize
import timber.log.Timber
import java.io.File
import javax.inject.Inject

class VideoContentRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder){
class VideoContentRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
private val errorFormatter: ErrorFormatter) {

// TODO DECRYPT_FILE Encrypted data
@Parcelize
data class Data(
val eventId: String,
val filename: String,
val videoUrl: String?,
val url: String?,
val elementToDecrypt: ElementToDecrypt?,
val thumbnailMediaData: ImageContentRenderer.Data
) : Parcelable

fun render(data: Data, thumbnailView: ImageView, videoView: VideoView) {
fun render(data: Data,
thumbnailView: ImageView,
loadingView: ProgressBar,
videoView: VideoView,
errorView: TextView) {
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
val resolvedUrl = contentUrlResolver.resolveFullSize(data.videoUrl)
videoView.setVideoPath(resolvedUrl)

if (data.elementToDecrypt != null) {
Timber.v("Decrypt video")
videoView.isVisible = false

if (data.url == null) {
loadingView.isVisible = false
errorView.isVisible = true
errorView.setText(R.string.unknown_error)
} else {
thumbnailView.isVisible = true
loadingView.isVisible = true

activeSessionHolder.getActiveSession()
.downloadFile(
FileService.DownloadMode.FOR_INTERNAL_USE,
data.eventId,
data.filename,
data.url,
data.elementToDecrypt,
object : MatrixCallback<File> {
override fun onSuccess(data: File) {
thumbnailView.isVisible = false
loadingView.isVisible = false
videoView.isVisible = true

videoView.setVideoPath(data.path)
videoView.start()
}

override fun onFailure(failure: Throwable) {
loadingView.isVisible = false
errorView.isVisible = true
errorView.text = errorFormatter.toHumanReadable(failure)
}
})
}
} else {
thumbnailView.isVisible = false
loadingView.isVisible = false

val resolvedUrl = contentUrlResolver.resolveFullSize(data.url)

if (resolvedUrl == null) {
errorView.isVisible = true
errorView.setText(R.string.unknown_error)
} else {
videoView.setVideoPath(resolvedUrl)
videoView.start()
}
}
}

}

View File

@ -28,6 +28,7 @@ import javax.inject.Inject

class VideoMediaViewerActivity : VectorBaseActivity() {

@Inject lateinit var imageContentRenderer: ImageContentRenderer
@Inject lateinit var videoContentRenderer: VideoContentRenderer

override fun injectWith(injector: ScreenComponent) {
@ -38,12 +39,10 @@ class VideoMediaViewerActivity : VectorBaseActivity() {
super.onCreate(savedInstanceState)
setContentView(im.vector.riotx.R.layout.activity_video_media_viewer)
val mediaData = intent.getParcelableExtra<VideoContentRenderer.Data>(EXTRA_MEDIA_DATA)
if (mediaData.videoUrl.isNullOrEmpty()) {
finish()
} else {

configureToolbar(videoMediaViewerToolbar, mediaData)
videoContentRenderer.render(mediaData, videoMediaViewerThumbnailView, videoMediaViewerVideoView)
}
imageContentRenderer.render(mediaData.thumbnailMediaData, ImageContentRenderer.Mode.FULL_SIZE, videoMediaViewerThumbnailView)
videoContentRenderer.render(mediaData, videoMediaViewerThumbnailView, videoMediaViewerLoading, videoMediaViewerVideoView, videoMediaViewerErrorView)
}

private fun configureToolbar(toolbar: Toolbar, mediaData: VideoContentRenderer.Data) {

View File

@ -15,6 +15,7 @@
-->

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
@ -32,12 +33,35 @@
<ImageView
android:id="@+id/videoMediaViewerThumbnailView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="match_parent"
android:visibility="gone"
tools:visibility="visible" />

<ProgressBar
android:id="@+id/videoMediaViewerLoading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"
tools:visibility="visible" />

<VideoView
android:id="@+id/videoMediaViewerVideoView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="match_parent"
android:visibility="gone" />

<TextView
android:id="@+id/videoMediaViewerErrorView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="16dp"
android:textColor="@color/riotx_notice"
android:textSize="16sp"
android:visibility="gone"
tools:text="Error"
tools:visibility="visible" />

</FrameLayout>


View File

@ -5,5 +5,14 @@

<string name="bottom_action_people_x">Direct Messages</string>

<string name="send_file_step_idle">Waiting…</string>
<string name="send_file_step_encrypting_thumbnail">Encrypting thumbnail…</string>
<string name="send_file_step_sending_thumbnail">Sending thumbnail (%1$s / %2$s)</string>
<string name="send_file_step_encrypting_file">Encrypting file…</string>
<string name="send_file_step_sending_file">Sending file (%1$s / %2$s)</string>

<string name="downloading_file">Downloading file %1$s…</string>
<string name="downloaded_file">File %1$s has been downloaded!</string>


</resources>