Merge pull request #318 from vector-im/feature/send_state
Fix some bugs on e2e rooms
This commit is contained in:
commit
a0bd206308
@ -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.ContentUploadStateTracker
|
||||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||||
|
import im.vector.matrix.android.api.session.file.FileService
|
||||||
import im.vector.matrix.android.api.session.group.GroupService
|
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.pushers.PushersService
|
||||||
import im.vector.matrix.android.api.session.room.RoomDirectoryService
|
import im.vector.matrix.android.api.session.room.RoomDirectoryService
|
||||||
@ -46,6 +47,7 @@ interface Session :
|
|||||||
CacheService,
|
CacheService,
|
||||||
SignOutService,
|
SignOutService,
|
||||||
FilterService,
|
FilterService,
|
||||||
|
FileService,
|
||||||
PushRuleService,
|
PushRuleService,
|
||||||
PushersService {
|
PushersService {
|
||||||
|
|
||||||
|
@ -28,10 +28,11 @@ interface ContentUploadStateTracker {
|
|||||||
|
|
||||||
sealed class State {
|
sealed class State {
|
||||||
object Idle : 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 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.api.session.events.model.Event
|
||||||
import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
|
import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
|
||||||
import im.vector.matrix.android.internal.crypto.NewSessionListener
|
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.ImportRoomKeysResult
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
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.MXEncryptEventContentResult
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
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.DevicesListResponse
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
interface CryptoService {
|
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.
|
* 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.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||||
@Json(name = "m.new_content") override val newContent: Content? = 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.
|
* 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
|
@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?
|
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
|
package im.vector.matrix.android.api.session.room.model.message
|
||||||
|
|
||||||
|
import android.content.ClipDescription
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import im.vector.matrix.android.api.session.events.model.Content
|
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.
|
* 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.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||||
@Json(name = "m.new_content") override val newContent: Content? = null,
|
@Json(name = "m.new_content") override val newContent: Content? = null,
|
||||||
|
|
||||||
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = 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.
|
* 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.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||||
@Json(name = "m.new_content") override val newContent: Content? = 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.
|
* 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
|
@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.
|
* 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.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||||
@Json(name = "m.new_content") override val newContent: Content? = 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.
|
* 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
|
@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.IMXEncrypting
|
||||||
import im.vector.matrix.android.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory
|
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.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.keysbackup.KeysBackup
|
||||||
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
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.MXDeviceInfo
|
||||||
@ -78,6 +79,7 @@ import im.vector.matrix.android.internal.util.fetchCopied
|
|||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import org.matrix.olm.OlmManager
|
import org.matrix.olm.OlmManager
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -16,8 +16,8 @@
|
|||||||
|
|
||||||
package im.vector.matrix.android.internal.crypto.attachments
|
package im.vector.matrix.android.internal.crypto.attachments
|
||||||
|
|
||||||
import android.text.TextUtils
|
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
|
import arrow.core.Try
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
|
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileKey
|
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileKey
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@ -51,7 +51,7 @@ object MXEncryptedAttachments {
|
|||||||
* @param mimetype the mime type
|
* @param mimetype the mime type
|
||||||
* @return the encryption file info
|
* @return the encryption file info
|
||||||
*/
|
*/
|
||||||
fun encryptAttachment(attachmentStream: InputStream, mimetype: String): EncryptionResult? {
|
fun encryptAttachment(attachmentStream: InputStream, mimetype: String): Try<EncryptionResult> {
|
||||||
val t0 = System.currentTimeMillis()
|
val t0 = System.currentTimeMillis()
|
||||||
val secureRandom = SecureRandom()
|
val secureRandom = SecureRandom()
|
||||||
|
|
||||||
@ -115,23 +115,21 @@ object MXEncryptedAttachments {
|
|||||||
encryptedByteArray = outStream.toByteArray()
|
encryptedByteArray = outStream.toByteArray()
|
||||||
)
|
)
|
||||||
|
|
||||||
outStream.close()
|
|
||||||
|
|
||||||
Timber.v("Encrypt in " + (System.currentTimeMillis() - t0) + " ms")
|
Timber.v("Encrypt in " + (System.currentTimeMillis() - t0) + " ms")
|
||||||
return result
|
return Try.just(result)
|
||||||
} catch (oom: OutOfMemoryError) {
|
} catch (oom: OutOfMemoryError) {
|
||||||
Timber.e(oom, "## encryptAttachment failed " + oom.message)
|
Timber.e(oom, "## encryptAttachment failed")
|
||||||
|
return Try.Failure(oom)
|
||||||
} catch (e: Exception) {
|
} 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))
|
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")
|
Timber.e("## decryptAttachment() : Digest value mismatch")
|
||||||
outStream.close()
|
outStream.close()
|
||||||
return null
|
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.annotation.MainThread
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import com.zhuinden.monarchy.Monarchy
|
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.auth.data.SessionParams
|
import im.vector.matrix.android.api.auth.data.SessionParams
|
||||||
import im.vector.matrix.android.api.pushrules.PushRuleService
|
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.ContentUploadStateTracker
|
||||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||||
|
import im.vector.matrix.android.api.session.file.FileService
|
||||||
import im.vector.matrix.android.api.session.group.GroupService
|
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.pushers.PushersService
|
||||||
import im.vector.matrix.android.api.session.room.RoomDirectoryService
|
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 pushRuleService: PushRuleService,
|
||||||
private val pushersService: PushersService,
|
private val pushersService: PushersService,
|
||||||
private val cryptoService: CryptoManager,
|
private val cryptoService: CryptoManager,
|
||||||
|
private val fileService: FileService,
|
||||||
private val syncThread: SyncThread,
|
private val syncThread: SyncThread,
|
||||||
private val contentUrlResolver: ContentUrlResolver,
|
private val contentUrlResolver: ContentUrlResolver,
|
||||||
private val contentUploadProgressTracker: ContentUploadStateTracker)
|
private val contentUploadProgressTracker: ContentUploadStateTracker)
|
||||||
: Session,
|
: Session,
|
||||||
RoomService by roomService,
|
RoomService by roomService,
|
||||||
RoomDirectoryService by roomDirectoryService,
|
RoomDirectoryService by roomDirectoryService,
|
||||||
GroupService by groupService,
|
GroupService by groupService,
|
||||||
UserService by userService,
|
UserService by userService,
|
||||||
CryptoService by cryptoService,
|
CryptoService by cryptoService,
|
||||||
CacheService by cacheService,
|
CacheService by cacheService,
|
||||||
SignOutService by signOutService,
|
SignOutService by signOutService,
|
||||||
FilterService by filterService,
|
FilterService by filterService,
|
||||||
PushRuleService by pushRuleService,
|
FileService by fileService,
|
||||||
PushersService by pushersService {
|
PushRuleService by pushRuleService,
|
||||||
|
PushersService by pushersService {
|
||||||
|
|
||||||
private var isOpen = false
|
private var isOpen = false
|
||||||
|
|
||||||
|
@ -43,8 +43,8 @@ internal class DefaultContentUploadStateTracker @Inject constructor() : ContentU
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun setFailure(key: String) {
|
internal fun setFailure(key: String, throwable: Throwable) {
|
||||||
val failure = ContentUploadStateTracker.State.Failure
|
val failure = ContentUploadStateTracker.State.Failure(throwable)
|
||||||
updateState(key, failure)
|
updateState(key, failure)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,8 +53,23 @@ internal class DefaultContentUploadStateTracker @Inject constructor() : ContentU
|
|||||||
updateState(key, success)
|
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) {
|
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)
|
updateState(key, progressData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
|
|||||||
val event: Event,
|
val event: Event,
|
||||||
val attachment: ContentAttachmentData,
|
val attachment: ContentAttachmentData,
|
||||||
val isRoomEncrypted: Boolean,
|
val isRoomEncrypted: Boolean,
|
||||||
override var lastFailureMessage: String? = null
|
override val lastFailureMessage: String? = null
|
||||||
) : SessionWorkerParams
|
) : SessionWorkerParams
|
||||||
|
|
||||||
@Inject lateinit var fileUploader: FileUploader
|
@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 eventId = params.event.eventId ?: return Result.success()
|
||||||
val attachment = params.attachment
|
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 uploadedThumbnailUrl: String? = null
|
||||||
var uploadedThumbnailEncryptedFileInfo: EncryptedFileInfo? = null
|
var uploadedThumbnailEncryptedFileInfo: EncryptedFileInfo? = null
|
||||||
|
|
||||||
if (thumbnailData != null) {
|
ThumbnailExtractor.extractThumbnail(params.attachment)?.let { thumbnailData ->
|
||||||
val contentUploadResponse = if (isRoomEncrypted) {
|
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")
|
Timber.v("Encrypt thumbnail")
|
||||||
val encryptionResult = MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType)
|
contentUploadStateTracker.setEncryptingThumbnail(eventId)
|
||||||
?: return Result.failure()
|
MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType)
|
||||||
|
.flatMap { encryptionResult ->
|
||||||
|
uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo
|
||||||
|
|
||||||
uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo
|
fileUploader
|
||||||
|
.uploadByteArray(encryptionResult.encryptedByteArray,
|
||||||
fileUploader
|
"thumb_${attachment.name}",
|
||||||
.uploadByteArray(encryptionResult.encryptedByteArray, "thumb_${attachment.name}", thumbnailData.mimeType)
|
"application/octet-stream",
|
||||||
|
thumbnailProgressListener)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
fileUploader
|
fileUploader
|
||||||
.uploadByteArray(thumbnailData.bytes, "thumb_${attachment.name}", thumbnailData.mimeType)
|
.uploadByteArray(thumbnailData.bytes,
|
||||||
|
"thumb_${attachment.name}",
|
||||||
|
thumbnailData.mimeType,
|
||||||
|
thumbnailProgressListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
contentUploadResponse
|
contentUploadResponse
|
||||||
@ -107,16 +127,17 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
|
|||||||
|
|
||||||
var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null
|
var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null
|
||||||
|
|
||||||
val contentUploadResponse = if (isRoomEncrypted) {
|
val contentUploadResponse = if (params.isRoomEncrypted) {
|
||||||
Timber.v("Encrypt file")
|
Timber.v("Encrypt file")
|
||||||
|
contentUploadStateTracker.setEncrypting(eventId)
|
||||||
|
|
||||||
val encryptionResult = MXEncryptedAttachments.encryptAttachment(FileInputStream(attachmentFile), attachment.mimeType)
|
MXEncryptedAttachments.encryptAttachment(FileInputStream(attachmentFile), attachment.mimeType)
|
||||||
?: return Result.failure()
|
.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 {
|
} else {
|
||||||
fileUploader
|
fileUploader
|
||||||
.uploadFile(attachmentFile, attachment.name, attachment.mimeType, progressListener)
|
.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 {
|
private fun handleFailure(params: Params, failure: Throwable): Result {
|
||||||
contentUploadStateTracker.setFailure(params.event.eventId!!)
|
contentUploadStateTracker.setFailure(params.event.eventId!!, failure)
|
||||||
return Result.success(
|
return Result.success(
|
||||||
WorkerParamsFactory.toData(
|
WorkerParamsFactory.toData(
|
||||||
params.copy(
|
params.copy(
|
||||||
@ -190,9 +202,10 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
|
|||||||
thumbnailEncryptedFileInfo: EncryptedFileInfo?): MessageVideoContent {
|
thumbnailEncryptedFileInfo: EncryptedFileInfo?): MessageVideoContent {
|
||||||
return copy(
|
return copy(
|
||||||
url = if (encryptedFileInfo == null) url else null,
|
url = if (encryptedFileInfo == null) url else null,
|
||||||
|
encryptedFileInfo = encryptedFileInfo?.copy(url = url),
|
||||||
videoInfo = videoInfo?.copy(
|
videoInfo = videoInfo?.copy(
|
||||||
thumbnailUrl = if (thumbnailEncryptedFileInfo == null) thumbnailUrl else null,
|
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(
|
internal data class Params(
|
||||||
override val userId: String,
|
override val userId: String,
|
||||||
val groupIds: List<String>,
|
val groupIds: List<String>,
|
||||||
override var lastFailureMessage: String? = null
|
override val lastFailureMessage: String? = null
|
||||||
) : SessionWorkerParams
|
) : SessionWorkerParams
|
||||||
|
|
||||||
@Inject lateinit var getGroupDataTask: GetGroupDataTask
|
@Inject lateinit var getGroupDataTask: GetGroupDataTask
|
||||||
|
@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.session.room
|
|||||||
import dagger.Binds
|
import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
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.RoomDirectoryService
|
||||||
import im.vector.matrix.android.api.session.room.RoomService
|
import im.vector.matrix.android.api.session.room.RoomService
|
||||||
import im.vector.matrix.android.api.session.room.members.MembershipService
|
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.send.SendService
|
||||||
import im.vector.matrix.android.api.session.room.state.StateService
|
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.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.SessionScope
|
||||||
import im.vector.matrix.android.internal.session.room.create.CreateRoomTask
|
import im.vector.matrix.android.internal.session.room.create.CreateRoomTask
|
||||||
import im.vector.matrix.android.internal.session.room.create.DefaultCreateRoomTask
|
import im.vector.matrix.android.internal.session.room.create.DefaultCreateRoomTask
|
||||||
@ -138,4 +140,6 @@ internal abstract class RoomModule {
|
|||||||
@Binds
|
@Binds
|
||||||
abstract fun bindTimelineService(timelineService: DefaultTimelineService): TimelineService
|
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 roomId: String,
|
||||||
val event: Event,
|
val event: Event,
|
||||||
val relationType: String? = null,
|
val relationType: String? = null,
|
||||||
override var lastFailureMessage: String?
|
override val lastFailureMessage: String?
|
||||||
) : SessionWorkerParams
|
) : SessionWorkerParams
|
||||||
|
|
||||||
@Inject lateinit var roomAPI: RoomAPI
|
@Inject lateinit var roomAPI: RoomAPI
|
||||||
|
@ -42,7 +42,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
|
|||||||
val event: Event,
|
val event: Event,
|
||||||
/**Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to)*/
|
/**Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to)*/
|
||||||
val keepKeys: List<String>? = null,
|
val keepKeys: List<String>? = null,
|
||||||
override var lastFailureMessage: String? = null
|
override val lastFailureMessage: String? = null
|
||||||
) : SessionWorkerParams
|
) : SessionWorkerParams
|
||||||
|
|
||||||
@Inject lateinit var crypto: CryptoService
|
@Inject lateinit var crypto: CryptoService
|
||||||
|
@ -36,7 +36,7 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters) : C
|
|||||||
val roomId: String,
|
val roomId: String,
|
||||||
val eventId: String,
|
val eventId: String,
|
||||||
val reason: String?,
|
val reason: String?,
|
||||||
override var lastFailureMessage: String? = null
|
override val lastFailureMessage: String? = null
|
||||||
) : SessionWorkerParams
|
) : SessionWorkerParams
|
||||||
|
|
||||||
@Inject lateinit var roomAPI: RoomAPI
|
@Inject lateinit var roomAPI: RoomAPI
|
||||||
|
@ -39,7 +39,7 @@ internal class SendEventWorker constructor(context: Context, params: WorkerParam
|
|||||||
override val userId: String,
|
override val userId: String,
|
||||||
val roomId: String,
|
val roomId: String,
|
||||||
val event: Event,
|
val event: Event,
|
||||||
override var lastFailureMessage: String? = null
|
override val lastFailureMessage: String? = null
|
||||||
) : SessionWorkerParams
|
) : SessionWorkerParams
|
||||||
|
|
||||||
@Inject lateinit var localEchoUpdater: LocalEchoUpdater
|
@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
|
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
|
// 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 okio.Okio
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save a string to a file with Okio
|
* 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_VIDEO_CALL = 572
|
||||||
const val PERMISSION_REQUEST_CODE_EXPORT_KEYS = 573
|
const val PERMISSION_REQUEST_CODE_EXPORT_KEYS = 573
|
||||||
const val PERMISSION_REQUEST_CODE_CHANGE_AVATAR = 574
|
const val PERMISSION_REQUEST_CODE_CHANGE_AVATAR = 574
|
||||||
|
const val PERMISSION_REQUEST_CODE_DOWNLOAD_FILE = 575
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log the used permissions statuses.
|
* 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.dialogs.ExportKeysDialog
|
||||||
import im.vector.riotx.core.extensions.observeEvent
|
import im.vector.riotx.core.extensions.observeEvent
|
||||||
import im.vector.riotx.core.platform.SimpleFragmentActivity
|
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
|
import im.vector.riotx.features.crypto.keys.KeysExporter
|
||||||
|
|
||||||
class KeysBackupSetupActivity : SimpleFragmentActivity() {
|
class KeysBackupSetupActivity : SimpleFragmentActivity() {
|
||||||
@ -132,39 +132,48 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun exportKeysManually() {
|
private fun exportKeysManually() {
|
||||||
ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener {
|
if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_EXPORT_KEYS)) {
|
||||||
override fun onPassphrase(passphrase: String) {
|
ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener {
|
||||||
showWaitingView()
|
override fun onPassphrase(passphrase: String) {
|
||||||
|
showWaitingView()
|
||||||
|
|
||||||
KeysExporter(session)
|
KeysExporter(session)
|
||||||
.export(this@KeysBackupSetupActivity,
|
.export(this@KeysBackupSetupActivity,
|
||||||
passphrase,
|
passphrase,
|
||||||
object : MatrixCallback<String> {
|
object : MatrixCallback<String> {
|
||||||
|
|
||||||
override fun onSuccess(data: String) {
|
override fun onSuccess(data: String) {
|
||||||
hideWaitingView()
|
hideWaitingView()
|
||||||
|
|
||||||
AlertDialog.Builder(this@KeysBackupSetupActivity)
|
AlertDialog.Builder(this@KeysBackupSetupActivity)
|
||||||
.setMessage(getString(R.string.encryption_export_saved_as, data))
|
.setMessage(getString(R.string.encryption_export_saved_as, data))
|
||||||
.setCancelable(false)
|
.setCancelable(false)
|
||||||
.setPositiveButton(R.string.ok) { dialog, which ->
|
.setPositiveButton(R.string.ok) { dialog, which ->
|
||||||
val resultIntent = Intent()
|
val resultIntent = Intent()
|
||||||
resultIntent.putExtra(MANUAL_EXPORT, true)
|
resultIntent.putExtra(MANUAL_EXPORT, true)
|
||||||
setResult(RESULT_OK, resultIntent)
|
setResult(RESULT_OK, resultIntent)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
override fun onFailure(failure: Throwable) {
|
||||||
toast(failure.localizedMessage)
|
toast(failure.localizedMessage)
|
||||||
hideWaitingView()
|
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() {
|
override fun onBackPressed() {
|
||||||
if (viewModel.shouldPromptOnBack) {
|
if (viewModel.shouldPromptOnBack) {
|
||||||
|
@ -18,6 +18,7 @@ package im.vector.riotx.features.home.room.detail
|
|||||||
|
|
||||||
import com.jaiselrahman.filepicker.model.MediaFile
|
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.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.Timeline
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
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 UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val add: Boolean) : RoomDetailActions()
|
||||||
data class ShowEditHistoryAction(val event: String, val editAggregatedSummary: EditAggregatedSummary) : RoomDetailActions()
|
data class ShowEditHistoryAction(val event: String, val editAggregatedSummary: EditAggregatedSummary) : RoomDetailActions()
|
||||||
data class NavigateToEvent(val eventId: String, val position: Int?) : 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 AcceptInvite : RoomDetailActions()
|
||||||
object RejectInvite : 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.di.ScreenComponent
|
||||||
import im.vector.riotx.core.dialogs.DialogListItem
|
import im.vector.riotx.core.dialogs.DialogListItem
|
||||||
import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer
|
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.hideKeyboard
|
||||||
import im.vector.riotx.core.extensions.observeEvent
|
import im.vector.riotx.core.extensions.observeEvent
|
||||||
import im.vector.riotx.core.extensions.setTextOrHide
|
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.glide.GlideApp
|
||||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||||
import im.vector.riotx.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
|
import im.vector.riotx.core.utils.*
|
||||||
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.features.autocomplete.command.AutocompleteCommandPresenter
|
import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter
|
||||||
import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy
|
import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy
|
||||||
import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter
|
import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter
|
||||||
@ -180,6 +175,7 @@ class RoomDetailFragment :
|
|||||||
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager
|
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager
|
||||||
@Inject lateinit var roomDetailViewModelFactory: RoomDetailViewModel.Factory
|
@Inject lateinit var roomDetailViewModelFactory: RoomDetailViewModel.Factory
|
||||||
@Inject lateinit var textComposerViewModelFactory: TextComposerViewModel.Factory
|
@Inject lateinit var textComposerViewModelFactory: TextComposerViewModel.Factory
|
||||||
|
@Inject lateinit var errorFormatter: ErrorFormatter
|
||||||
private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
|
private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
|
||||||
private lateinit var scrollOnHighlightedEventCallback: ScrollOnHighlightedEventCallback
|
private lateinit var scrollOnHighlightedEventCallback: ScrollOnHighlightedEventCallback
|
||||||
|
|
||||||
@ -220,6 +216,15 @@ class RoomDetailFragment :
|
|||||||
scrollOnHighlightedEventCallback.scheduleScrollTo(it)
|
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(
|
roomDetailViewModel.selectSubscribe(
|
||||||
RoomDetailViewState::sendMode,
|
RoomDetailViewState::sendMode,
|
||||||
RoomDetailViewState::selectedEvent,
|
RoomDetailViewState::selectedEvent,
|
||||||
@ -615,8 +620,27 @@ class RoomDetailFragment :
|
|||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFileMessageClicked(messageFileContent: MessageFileContent) {
|
override fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) {
|
||||||
vectorBaseActivity.notImplemented("open file")
|
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) {
|
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.Session
|
||||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
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.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.Membership
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||||
|
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.api.session.room.timeline.TimelineEvent
|
||||||
|
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
|
||||||
import im.vector.matrix.rx.rx
|
import im.vector.matrix.rx.rx
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.intent.getFilenameFromUri
|
import im.vector.riotx.core.intent.getFilenameFromUri
|
||||||
@ -50,6 +53,7 @@ import io.reactivex.rxkotlin.subscribeBy
|
|||||||
import org.commonmark.parser.Parser
|
import org.commonmark.parser.Parser
|
||||||
import org.commonmark.renderer.html.HtmlRenderer
|
import org.commonmark.renderer.html.HtmlRenderer
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@ -71,6 +75,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||||||
}
|
}
|
||||||
private var timeline = room.createTimeline(eventId, allowedTypes)
|
private var timeline = room.createTimeline(eventId, allowedTypes)
|
||||||
|
|
||||||
|
// Slot to keep a pending action during permission request
|
||||||
|
var pendingAction: RoomDetailActions? = null
|
||||||
|
|
||||||
@AssistedInject.Factory
|
@AssistedInject.Factory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(initialState: RoomDetailViewState): RoomDetailViewModel
|
fun create(initialState: RoomDetailViewState): RoomDetailViewModel
|
||||||
@ -113,6 +120,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||||||
is RoomDetailActions.EnterEditMode -> handleEditAction(action)
|
is RoomDetailActions.EnterEditMode -> handleEditAction(action)
|
||||||
is RoomDetailActions.EnterQuoteMode -> handleQuoteAction(action)
|
is RoomDetailActions.EnterQuoteMode -> handleQuoteAction(action)
|
||||||
is RoomDetailActions.EnterReplyMode -> handleReplyAction(action)
|
is RoomDetailActions.EnterReplyMode -> handleReplyAction(action)
|
||||||
|
is RoomDetailActions.DownloadFile -> handleDownloadFile(action)
|
||||||
is RoomDetailActions.NavigateToEvent -> handleNavigateToEvent(action)
|
is RoomDetailActions.NavigateToEvent -> handleNavigateToEvent(action)
|
||||||
else -> Timber.e("Unhandled Action: $action")
|
else -> Timber.e("Unhandled Action: $action")
|
||||||
}
|
}
|
||||||
@ -149,6 +157,10 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||||||
val navigateToEvent: LiveData<LiveEvent<String>>
|
val navigateToEvent: LiveData<LiveEvent<String>>
|
||||||
get() = _navigateToEvent
|
get() = _navigateToEvent
|
||||||
|
|
||||||
|
private val _downloadedFileEvent = MutableLiveData<LiveEvent<DownloadFileState>>()
|
||||||
|
val downloadedFileEvent: LiveData<LiveEvent<DownloadFileState>>
|
||||||
|
get() = _downloadedFileEvent
|
||||||
|
|
||||||
|
|
||||||
// PRIVATE METHODS *****************************************************************************
|
// 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) {
|
private fun handleNavigateToEvent(action: RoomDetailActions.NavigateToEvent) {
|
||||||
val targetEventId = action.eventId
|
val targetEventId = action.eventId
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
|
|||||||
fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View)
|
fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View)
|
||||||
fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View)
|
fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View)
|
||||||
fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.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 onAudioMessageClicked(messageAudioContent: MessageAudioContent)
|
||||||
fun onEditedDecorationClicked(informationData: MessageInformationData, editAggregatedSummary: EditAggregatedSummary?)
|
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.RelationType
|
||||||
import im.vector.matrix.android.api.session.events.model.toModel
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary
|
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.*
|
||||||
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.send.SendState
|
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.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
|
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
|
||||||
@ -87,9 +80,9 @@ class MessageItemFactory @Inject constructor(
|
|||||||
|
|
||||||
val messageContent: MessageContent =
|
val messageContent: MessageContent =
|
||||||
event.annotations?.editSummary?.aggregatedContent?.toModel()
|
event.annotations?.editSummary?.aggregatedContent?.toModel()
|
||||||
?: event.root.getClearContent().toModel()
|
?: event.root.getClearContent().toModel()
|
||||||
?: //Malformed content, we should echo something on screen
|
?: //Malformed content, we should echo something on screen
|
||||||
return DefaultItem_().text(stringProvider.getString(R.string.malformed_message))
|
return DefaultItem_().text(stringProvider.getString(R.string.malformed_message))
|
||||||
|
|
||||||
if (messageContent.relatesTo?.type == RelationType.REPLACE) {
|
if (messageContent.relatesTo?.type == RelationType.REPLACE) {
|
||||||
// ignore replace event, the targeted id is already edited
|
// ignore replace event, the targeted id is already edited
|
||||||
@ -99,16 +92,16 @@ class MessageItemFactory @Inject constructor(
|
|||||||
// val ev = all.toModel<Event>()
|
// val ev = all.toModel<Event>()
|
||||||
return when (messageContent) {
|
return when (messageContent) {
|
||||||
is MessageEmoteContent -> buildEmoteMessageItem(messageContent,
|
is MessageEmoteContent -> buildEmoteMessageItem(messageContent,
|
||||||
informationData,
|
informationData,
|
||||||
event.annotations?.editSummary,
|
event.annotations?.editSummary,
|
||||||
highlight,
|
highlight,
|
||||||
callback)
|
callback)
|
||||||
is MessageTextContent -> buildTextMessageItem(event.sendState,
|
is MessageTextContent -> buildTextMessageItem(event.sendState,
|
||||||
messageContent,
|
messageContent,
|
||||||
informationData,
|
informationData,
|
||||||
event.annotations?.editSummary,
|
event.annotations?.editSummary,
|
||||||
highlight,
|
highlight,
|
||||||
callback
|
callback
|
||||||
)
|
)
|
||||||
is MessageImageContent -> buildImageMessageItem(messageContent, informationData, highlight, callback)
|
is MessageImageContent -> buildImageMessageItem(messageContent, informationData, highlight, callback)
|
||||||
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback)
|
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback)
|
||||||
@ -142,7 +135,7 @@ class MessageItemFactory @Inject constructor(
|
|||||||
}))
|
}))
|
||||||
.longClickListener { view ->
|
.longClickListener { view ->
|
||||||
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
|
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
|
||||||
?: false
|
?: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,12 +158,16 @@ class MessageItemFactory @Inject constructor(
|
|||||||
}))
|
}))
|
||||||
.longClickListener { view ->
|
.longClickListener { view ->
|
||||||
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
|
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
|
||||||
?: false
|
?: false
|
||||||
}
|
}
|
||||||
.clickListener(
|
.clickListener(
|
||||||
DebouncedClickListener(View.OnClickListener { _ ->
|
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? {
|
private fun buildNotHandledMessageItem(messageContent: MessageContent, highlight: Boolean): DefaultItem? {
|
||||||
@ -188,7 +185,7 @@ class MessageItemFactory @Inject constructor(
|
|||||||
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
|
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
|
||||||
val data = ImageContentRenderer.Data(
|
val data = ImageContentRenderer.Data(
|
||||||
filename = messageContent.body,
|
filename = messageContent.body,
|
||||||
url = messageContent.encryptedFileInfo?.url ?: messageContent.url,
|
url = messageContent.getFileUrl(),
|
||||||
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(),
|
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(),
|
||||||
height = messageContent.info?.height,
|
height = messageContent.info?.height,
|
||||||
maxHeight = maxHeight,
|
maxHeight = maxHeight,
|
||||||
@ -218,7 +215,7 @@ class MessageItemFactory @Inject constructor(
|
|||||||
}))
|
}))
|
||||||
.longClickListener { view ->
|
.longClickListener { view ->
|
||||||
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
|
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
|
||||||
?: false
|
?: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -239,8 +236,10 @@ class MessageItemFactory @Inject constructor(
|
|||||||
)
|
)
|
||||||
|
|
||||||
val videoData = VideoContentRenderer.Data(
|
val videoData = VideoContentRenderer.Data(
|
||||||
|
eventId = informationData.eventId,
|
||||||
filename = messageContent.body,
|
filename = messageContent.body,
|
||||||
videoUrl = messageContent.url,
|
url = messageContent.getFileUrl(),
|
||||||
|
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(),
|
||||||
thumbnailMediaData = thumbnailData
|
thumbnailMediaData = thumbnailData
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -262,7 +261,7 @@ class MessageItemFactory @Inject constructor(
|
|||||||
.clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view) }
|
.clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view) }
|
||||||
.longClickListener { view ->
|
.longClickListener { view ->
|
||||||
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
|
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
|
||||||
?: false
|
?: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -302,7 +301,7 @@ class MessageItemFactory @Inject constructor(
|
|||||||
}))
|
}))
|
||||||
.longClickListener { view ->
|
.longClickListener { view ->
|
||||||
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
|
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
|
||||||
?: false
|
?: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -334,9 +333,9 @@ class MessageItemFactory @Inject constructor(
|
|||||||
//nop
|
//nop
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
editStart,
|
editStart,
|
||||||
editEnd,
|
editEnd,
|
||||||
Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
|
Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
|
||||||
return spannable
|
return spannable
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -372,7 +371,7 @@ class MessageItemFactory @Inject constructor(
|
|||||||
}))
|
}))
|
||||||
.longClickListener { view ->
|
.longClickListener { view ->
|
||||||
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
|
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
|
||||||
?: false
|
?: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -408,7 +407,7 @@ class MessageItemFactory @Inject constructor(
|
|||||||
}))
|
}))
|
||||||
.longClickListener { view ->
|
.longClickListener { view ->
|
||||||
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
|
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
|
||||||
?: false
|
?: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,17 +16,16 @@
|
|||||||
|
|
||||||
package im.vector.riotx.features.home.room.detail.timeline.helper
|
package im.vector.riotx.features.home.room.detail.timeline.helper
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.text.format.Formatter
|
import android.text.format.Formatter
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.di.ActiveSessionHolder
|
import im.vector.riotx.core.di.ActiveSessionHolder
|
||||||
import im.vector.riotx.features.media.ImageContentRenderer
|
import im.vector.riotx.features.media.ImageContentRenderer
|
||||||
import java.io.File
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ContentUploadStateTrackerBinder @Inject constructor(private val activeSessionHolder: ActiveSessionHolder) {
|
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) {
|
override fun onUpdate(state: ContentUploadStateTracker.State) {
|
||||||
when (state) {
|
when (state) {
|
||||||
is ContentUploadStateTracker.State.Idle -> handleIdle(state)
|
is ContentUploadStateTracker.State.Idle -> handleIdle(state)
|
||||||
is ContentUploadStateTracker.State.Failure -> handleFailure(state)
|
is ContentUploadStateTracker.State.EncryptingThumbnail -> handleEncryptingThumbnail(state)
|
||||||
is ContentUploadStateTracker.State.Success -> handleSuccess(state)
|
is ContentUploadStateTracker.State.UploadingThumbnail -> handleProgressThumbnail(state)
|
||||||
is ContentUploadStateTracker.State.ProgressData -> handleProgress(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) {
|
private fun handleIdle(state: ContentUploadStateTracker.State.Idle) {
|
||||||
if (mediaData.isLocalFile()) {
|
if (mediaData.isLocalFile()) {
|
||||||
val file = File(mediaData.url)
|
progressLayout.isVisible = true
|
||||||
progressLayout.visibility = View.VISIBLE
|
|
||||||
val progressBar = progressLayout.findViewById<ProgressBar>(R.id.mediaProgressBar)
|
val progressBar = progressLayout.findViewById<ProgressBar>(R.id.mediaProgressBar)
|
||||||
val progressTextView = progressLayout.findViewById<TextView>(R.id.mediaProgressTextView)
|
val progressTextView = progressLayout.findViewById<TextView>(R.id.mediaProgressTextView)
|
||||||
|
progressBar?.isVisible = true
|
||||||
|
progressBar?.isIndeterminate = true
|
||||||
progressBar?.progress = 0
|
progressBar?.progress = 0
|
||||||
progressTextView?.text = formatStats(progressLayout.context, 0L, file.length())
|
progressTextView?.text = progressLayout.context.getString(R.string.send_file_step_idle)
|
||||||
} else {
|
} 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) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO DECRYPT_FILE Decrypt file
|
|
||||||
imageView.showImage(
|
imageView.showImage(
|
||||||
Uri.parse(thumbnail),
|
Uri.parse(thumbnail),
|
||||||
Uri.parse(fullSize)
|
Uri.parse(fullSize)
|
||||||
|
@ -18,26 +18,90 @@ package im.vector.riotx.features.media
|
|||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
import android.widget.ProgressBar
|
||||||
|
import android.widget.TextView
|
||||||
import android.widget.VideoView
|
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.di.ActiveSessionHolder
|
||||||
|
import im.vector.riotx.core.error.ErrorFormatter
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
import javax.inject.Inject
|
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
|
@Parcelize
|
||||||
data class Data(
|
data class Data(
|
||||||
|
val eventId: String,
|
||||||
val filename: String,
|
val filename: String,
|
||||||
val videoUrl: String?,
|
val url: String?,
|
||||||
|
val elementToDecrypt: ElementToDecrypt?,
|
||||||
val thumbnailMediaData: ImageContentRenderer.Data
|
val thumbnailMediaData: ImageContentRenderer.Data
|
||||||
) : Parcelable
|
) : 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 contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
|
||||||
val resolvedUrl = contentUrlResolver.resolveFullSize(data.videoUrl)
|
|
||||||
videoView.setVideoPath(resolvedUrl)
|
if (data.elementToDecrypt != null) {
|
||||||
videoView.start()
|
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() {
|
class VideoMediaViewerActivity : VectorBaseActivity() {
|
||||||
|
|
||||||
|
@Inject lateinit var imageContentRenderer: ImageContentRenderer
|
||||||
@Inject lateinit var videoContentRenderer: VideoContentRenderer
|
@Inject lateinit var videoContentRenderer: VideoContentRenderer
|
||||||
|
|
||||||
override fun injectWith(injector: ScreenComponent) {
|
override fun injectWith(injector: ScreenComponent) {
|
||||||
@ -38,12 +39,10 @@ class VideoMediaViewerActivity : VectorBaseActivity() {
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(im.vector.riotx.R.layout.activity_video_media_viewer)
|
setContentView(im.vector.riotx.R.layout.activity_video_media_viewer)
|
||||||
val mediaData = intent.getParcelableExtra<VideoContentRenderer.Data>(EXTRA_MEDIA_DATA)
|
val mediaData = intent.getParcelableExtra<VideoContentRenderer.Data>(EXTRA_MEDIA_DATA)
|
||||||
if (mediaData.videoUrl.isNullOrEmpty()) {
|
|
||||||
finish()
|
configureToolbar(videoMediaViewerToolbar, mediaData)
|
||||||
} else {
|
imageContentRenderer.render(mediaData.thumbnailMediaData, ImageContentRenderer.Mode.FULL_SIZE, videoMediaViewerThumbnailView)
|
||||||
configureToolbar(videoMediaViewerToolbar, mediaData)
|
videoContentRenderer.render(mediaData, videoMediaViewerThumbnailView, videoMediaViewerLoading, videoMediaViewerVideoView, videoMediaViewerErrorView)
|
||||||
videoContentRenderer.render(mediaData, videoMediaViewerThumbnailView, videoMediaViewerVideoView)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun configureToolbar(toolbar: Toolbar, mediaData: VideoContentRenderer.Data) {
|
private fun configureToolbar(toolbar: Toolbar, mediaData: VideoContentRenderer.Data) {
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
@ -32,12 +33,35 @@
|
|||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/videoMediaViewerThumbnailView"
|
android:id="@+id/videoMediaViewerThumbnailView"
|
||||||
android:layout_width="match_parent"
|
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
|
<VideoView
|
||||||
android:id="@+id/videoMediaViewerVideoView"
|
android:id="@+id/videoMediaViewerVideoView"
|
||||||
android:layout_width="match_parent"
|
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>
|
</FrameLayout>
|
||||||
|
|
||||||
|
@ -5,5 +5,14 @@
|
|||||||
|
|
||||||
<string name="bottom_action_people_x">Direct Messages</string>
|
<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>
|
</resources>
|
Loading…
Reference in New Issue
Block a user