mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-02-17 04:20:00 +03:00
Merge pull request #318 from vector-im/feature/send_state
Fix some bugs on e2e rooms
This commit is contained in:
commit
a0bd206308
37 changed files with 677 additions and 197 deletions
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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>)
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,20 +61,22 @@ 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)
|
||||
: Session,
|
||||
RoomService by roomService,
|
||||
RoomDirectoryService by roomDirectoryService,
|
||||
GroupService by groupService,
|
||||
UserService by userService,
|
||||
CryptoService by cryptoService,
|
||||
CacheService by cacheService,
|
||||
SignOutService by signOutService,
|
||||
FilterService by filterService,
|
||||
PushRuleService by pushRuleService,
|
||||
PushersService by pushersService {
|
||||
RoomService by roomService,
|
||||
RoomDirectoryService by roomDirectoryService,
|
||||
GroupService by groupService,
|
||||
UserService by userService,
|
||||
CryptoService by cryptoService,
|
||||
CacheService by cacheService,
|
||||
SignOutService by signOutService,
|
||||
FilterService by filterService,
|
||||
FileService by fileService,
|
||||
PushRuleService by pushRuleService,
|
||||
PushersService by pushersService {
|
||||
|
||||
private var isOpen = false
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
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")
|
||||
val encryptionResult = MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType)
|
||||
?: return Result.failure()
|
||||
contentUploadStateTracker.setEncryptingThumbnail(eventId)
|
||||
MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType)
|
||||
.flatMap { encryptionResult ->
|
||||
uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo
|
||||
|
||||
uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo
|
||||
|
||||
fileUploader
|
||||
.uploadByteArray(encryptionResult.encryptedByteArray, "thumb_${attachment.name}", thumbnailData.mimeType)
|
||||
fileUploader
|
||||
.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
|
||||
|
||||
uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo
|
||||
|
||||
fileUploader
|
||||
.uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener)
|
||||
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)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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?
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,39 +132,48 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
|
|||
}
|
||||
|
||||
private fun exportKeysManually() {
|
||||
ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener {
|
||||
override fun onPassphrase(passphrase: String) {
|
||||
showWaitingView()
|
||||
if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_EXPORT_KEYS)) {
|
||||
ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener {
|
||||
override fun onPassphrase(passphrase: String) {
|
||||
showWaitingView()
|
||||
|
||||
KeysExporter(session)
|
||||
.export(this@KeysBackupSetupActivity,
|
||||
passphrase,
|
||||
object : MatrixCallback<String> {
|
||||
KeysExporter(session)
|
||||
.export(this@KeysBackupSetupActivity,
|
||||
passphrase,
|
||||
object : MatrixCallback<String> {
|
||||
|
||||
override fun onSuccess(data: String) {
|
||||
hideWaitingView()
|
||||
override fun onSuccess(data: String) {
|
||||
hideWaitingView()
|
||||
|
||||
AlertDialog.Builder(this@KeysBackupSetupActivity)
|
||||
.setMessage(getString(R.string.encryption_export_saved_as, data))
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.ok) { dialog, which ->
|
||||
val resultIntent = Intent()
|
||||
resultIntent.putExtra(MANUAL_EXPORT, true)
|
||||
setResult(RESULT_OK, resultIntent)
|
||||
finish()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
AlertDialog.Builder(this@KeysBackupSetupActivity)
|
||||
.setMessage(getString(R.string.encryption_export_saved_as, data))
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.ok) { dialog, which ->
|
||||
val resultIntent = Intent()
|
||||
resultIntent.putExtra(MANUAL_EXPORT, true)
|
||||
setResult(RESULT_OK, resultIntent)
|
||||
finish()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
toast(failure.localizedMessage)
|
||||
hideWaitingView()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
override fun onFailure(failure: Throwable) {
|
||||
toast(failure.localizedMessage)
|
||||
hideWaitingView()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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?)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
@ -87,9 +80,9 @@ class MessageItemFactory @Inject constructor(
|
|||
|
||||
val messageContent: MessageContent =
|
||||
event.annotations?.editSummary?.aggregatedContent?.toModel()
|
||||
?: event.root.getClearContent().toModel()
|
||||
?: //Malformed content, we should echo something on screen
|
||||
return DefaultItem_().text(stringProvider.getString(R.string.malformed_message))
|
||||
?: event.root.getClearContent().toModel()
|
||||
?: //Malformed content, we should echo something on screen
|
||||
return DefaultItem_().text(stringProvider.getString(R.string.malformed_message))
|
||||
|
||||
if (messageContent.relatesTo?.type == RelationType.REPLACE) {
|
||||
// ignore replace event, the targeted id is already edited
|
||||
|
@ -99,16 +92,16 @@ class MessageItemFactory @Inject constructor(
|
|||
// val ev = all.toModel<Event>()
|
||||
return when (messageContent) {
|
||||
is MessageEmoteContent -> buildEmoteMessageItem(messageContent,
|
||||
informationData,
|
||||
event.annotations?.editSummary,
|
||||
highlight,
|
||||
callback)
|
||||
informationData,
|
||||
event.annotations?.editSummary,
|
||||
highlight,
|
||||
callback)
|
||||
is MessageTextContent -> buildTextMessageItem(event.sendState,
|
||||
messageContent,
|
||||
informationData,
|
||||
event.annotations?.editSummary,
|
||||
highlight,
|
||||
callback
|
||||
messageContent,
|
||||
informationData,
|
||||
event.annotations?.editSummary,
|
||||
highlight,
|
||||
callback
|
||||
)
|
||||
is MessageImageContent -> buildImageMessageItem(messageContent, informationData, highlight, callback)
|
||||
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback)
|
||||
|
@ -142,7 +135,7 @@ class MessageItemFactory @Inject constructor(
|
|||
}))
|
||||
.longClickListener { view ->
|
||||
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
|
||||
?: false
|
||||
?: false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -165,12 +158,16 @@ class MessageItemFactory @Inject constructor(
|
|||
}))
|
||||
.longClickListener { view ->
|
||||
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
|
||||
?: false
|
||||
?: false
|
||||
}
|
||||
.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,
|
||||
|
@ -218,7 +215,7 @@ class MessageItemFactory @Inject constructor(
|
|||
}))
|
||||
.longClickListener { view ->
|
||||
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
|
||||
?: false
|
||||
?: false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
)
|
||||
|
||||
|
@ -262,7 +261,7 @@ class MessageItemFactory @Inject constructor(
|
|||
.clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view) }
|
||||
.longClickListener { view ->
|
||||
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
|
||||
?: false
|
||||
?: false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -302,7 +301,7 @@ class MessageItemFactory @Inject constructor(
|
|||
}))
|
||||
.longClickListener { view ->
|
||||
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
|
||||
?: false
|
||||
?: false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -334,9 +333,9 @@ class MessageItemFactory @Inject constructor(
|
|||
//nop
|
||||
}
|
||||
},
|
||||
editStart,
|
||||
editEnd,
|
||||
Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
|
||||
editStart,
|
||||
editEnd,
|
||||
Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
|
||||
return spannable
|
||||
}
|
||||
|
||||
|
@ -372,7 +371,7 @@ class MessageItemFactory @Inject constructor(
|
|||
}))
|
||||
.longClickListener { view ->
|
||||
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
|
||||
?: false
|
||||
?: false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -408,7 +407,7 @@ class MessageItemFactory @Inject constructor(
|
|||
}))
|
||||
.longClickListener { view ->
|
||||
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
|
||||
?: false
|
||||
?: false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
@ -61,45 +60,77 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup,
|
|||
|
||||
override fun onUpdate(state: ContentUploadStateTracker.State) {
|
||||
when (state) {
|
||||
is ContentUploadStateTracker.State.Idle -> handleIdle(state)
|
||||
is ContentUploadStateTracker.State.Failure -> handleFailure(state)
|
||||
is ContentUploadStateTracker.State.Success -> handleSuccess(state)
|
||||
is ContentUploadStateTracker.State.ProgressData -> handleProgress(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)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleIdle(state: ContentUploadStateTracker.State.Idle) {
|
||||
if (mediaData.isLocalFile()) {
|
||||
val file = File(mediaData.url)
|
||||
progressLayout.visibility = View.VISIBLE
|
||||
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 = formatStats(progressLayout.context, 0L, file.length())
|
||||
progressTextView?.text = progressLayout.context.getString(R.string.send_file_step_idle)
|
||||
} else {
|
||||
progressLayout.visibility = View.GONE
|
||||
progressLayout.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleFailure(state: ContentUploadStateTracker.State.Failure) {
|
||||
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?.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)}"
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
videoView.start()
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
configureToolbar(videoMediaViewerToolbar, mediaData)
|
||||
imageContentRenderer.render(mediaData.thumbnailMediaData, ImageContentRenderer.Mode.FULL_SIZE, videoMediaViewerThumbnailView)
|
||||
videoContentRenderer.render(mediaData, videoMediaViewerThumbnailView, videoMediaViewerLoading, videoMediaViewerVideoView, videoMediaViewerErrorView)
|
||||
}
|
||||
|
||||
private fun configureToolbar(toolbar: Toolbar, mediaData: VideoContentRenderer.Data) {
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
Loading…
Add table
Reference in a new issue