Rebase post matrix sdk package renaming

This commit is contained in:
Valere 2020-08-17 23:33:33 +02:00
parent cc57a73f23
commit bfcbb9ff4f
28 changed files with 563 additions and 231 deletions

View File

@ -29,6 +29,8 @@ import org.junit.runners.MethodSorters
import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileKey import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileKey
import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt
import java.io.ByteArrayInputStream
import java.io.InputStream import java.io.InputStream
/** /**
@ -52,7 +54,7 @@ class AttachmentEncryptionTest {
memoryFile.inputStream memoryFile.inputStream
} }
val decryptedStream = MXEncryptedAttachments.decryptAttachment(inputStream, encryptedFileInfo) val decryptedStream = MXEncryptedAttachments.decryptAttachment(inputStream, encryptedFileInfo.toElementToDecrypt()!!)
assertNotNull(decryptedStream) assertNotNull(decryptedStream)

View File

@ -33,7 +33,7 @@ interface ContentUploadStateTracker {
object Idle : State() object Idle : State()
object EncryptingThumbnail : State() object EncryptingThumbnail : State()
data class UploadingThumbnail(val current: Long, val total: Long) : State() data class UploadingThumbnail(val current: Long, val total: Long) : State()
object Encrypting : State() data class Encrypting(val current: Long, val total: Long) : State()
data class Uploading(val current: Long, val total: Long) : State() data class Uploading(val current: Long, val total: Long) : State()
object Success : State() object Success : State()
data class Failure(val throwable: Throwable) : State() data class Failure(val throwable: Throwable) : State()

View File

@ -239,6 +239,14 @@ fun Event.isVideoMessage(): Boolean {
} }
} }
fun Event.isAudioMessage(): Boolean {
return getClearType() == EventType.MESSAGE
&& when (getClearContent()?.toModel<MessageContent>()?.msgType) {
MessageType.MSGTYPE_AUDIO -> true
else -> false
}
}
fun Event.isFileMessage(): Boolean { fun Event.isFileMessage(): Boolean {
return getClearType() == EventType.MESSAGE return getClearType() == EventType.MESSAGE
&& when (getClearContent()?.toModel<MessageContent>()?.msgType) { && when (getClearContent()?.toModel<MessageContent>()?.msgType) {
@ -246,6 +254,16 @@ fun Event.isFileMessage(): Boolean {
else -> false else -> false
} }
} }
fun Event.isAttachmentMessage(): Boolean {
return getClearType() == EventType.MESSAGE
&& when (getClearContent()?.toModel<MessageContent>()?.msgType) {
MessageType.MSGTYPE_IMAGE,
MessageType.MSGTYPE_AUDIO,
MessageType.MSGTYPE_VIDEO,
MessageType.MSGTYPE_FILE -> true
else -> false
}
}
fun Event.getRelationContent(): RelationDefaultContent? { fun Event.getRelationContent(): RelationDefaultContent? {
return if (isEncrypted()) { return if (isEncrypted()) {

View File

@ -126,6 +126,8 @@ interface SendService {
fun clearSendingQueue() fun clearSendingQueue()
fun cancelSend(eventId: String)
/** /**
* Resend all failed messages one by one (and keep order) * Resend all failed messages one by one (and keep order)
*/ */

View File

@ -37,7 +37,8 @@ enum class SendState {
internal companion object { internal companion object {
val HAS_FAILED_STATES = listOf(UNDELIVERED, FAILED_UNKNOWN_DEVICES) val HAS_FAILED_STATES = listOf(UNDELIVERED, FAILED_UNKNOWN_DEVICES)
val IS_SENT_STATES = listOf(SENT, SYNCED) val IS_SENT_STATES = listOf(SENT, SYNCED)
val IS_SENDING_STATES = listOf(UNSENT, ENCRYPTING, SENDING) val IS_PROGRESSING_STATES = listOf(ENCRYPTING, SENDING)
val IS_SENDING_STATES = IS_PROGRESSING_STATES + UNSENT
val PENDING_STATES = IS_SENDING_STATES + HAS_FAILED_STATES val PENDING_STATES = IS_SENDING_STATES + HAS_FAILED_STATES
} }
@ -45,5 +46,7 @@ enum class SendState {
fun hasFailed() = HAS_FAILED_STATES.contains(this) fun hasFailed() = HAS_FAILED_STATES.contains(this)
fun isInProgress() = IS_PROGRESSING_STATES.contains(this)
fun isSending() = IS_SENDING_STATES.contains(this) fun isSending() = IS_SENDING_STATES.contains(this)
} }

View File

@ -22,10 +22,13 @@ import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileKey import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileKey
import timber.log.Timber import timber.log.Timber
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.File
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream
import java.security.MessageDigest import java.security.MessageDigest
import java.security.SecureRandom import java.security.SecureRandom
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.CipherInputStream
import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
@ -35,8 +38,121 @@ internal object MXEncryptedAttachments {
private const val SECRET_KEY_SPEC_ALGORITHM = "AES" private const val SECRET_KEY_SPEC_ALGORITHM = "AES"
private const val MESSAGE_DIGEST_ALGORITHM = "SHA-256" private const val MESSAGE_DIGEST_ALGORITHM = "SHA-256"
fun encrypt(clearStream: InputStream, mimetype: String?, outputFile: File, progress: ((current: Int, total: Int) -> Unit)): EncryptedFileInfo {
val t0 = System.currentTimeMillis()
val secureRandom = SecureRandom()
val initVectorBytes = ByteArray(16) { 0.toByte() }
val ivRandomPart = ByteArray(8)
secureRandom.nextBytes(ivRandomPart)
System.arraycopy(ivRandomPart, 0, initVectorBytes, 0, ivRandomPart.size)
val key = ByteArray(32)
secureRandom.nextBytes(key)
val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM)
outputFile.outputStream().use { outputStream ->
val encryptCipher = Cipher.getInstance(CIPHER_ALGORITHM)
val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM)
val ivParameterSpec = IvParameterSpec(initVectorBytes)
encryptCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec)
val data = ByteArray(CRYPTO_BUFFER_SIZE)
var read: Int
var encodedBytes: ByteArray
clearStream.use { inputStream ->
val estimatedSize = inputStream.available()
progress.invoke(0, estimatedSize)
read = inputStream.read(data)
var totalRead = read
while (read != -1) {
progress.invoke(totalRead, estimatedSize)
encodedBytes = encryptCipher.update(data, 0, read)
messageDigest.update(encodedBytes, 0, encodedBytes.size)
outputStream.write(encodedBytes)
read = inputStream.read(data)
totalRead += read
}
}
// encrypt the latest chunk
encodedBytes = encryptCipher.doFinal()
messageDigest.update(encodedBytes, 0, encodedBytes.size)
outputStream.write(encodedBytes)
}
return EncryptedFileInfo(
url = null,
mimetype = mimetype,
key = EncryptedFileKey(
alg = "A256CTR",
ext = true,
key_ops = listOf("encrypt", "decrypt"),
kty = "oct",
k = base64ToBase64Url(Base64.encodeToString(key, Base64.DEFAULT))
),
iv = Base64.encodeToString(initVectorBytes, Base64.DEFAULT).replace("\n", "").replace("=", ""),
hashes = mapOf("sha256" to base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))),
v = "v2"
)
.also { Timber.v("Encrypt in ${System.currentTimeMillis() - t0}ms") }
}
// fun cipherInputStream(attachmentStream: InputStream, mimetype: String?): Pair<DigestInputStream, EncryptedFileInfo> {
// val secureRandom = SecureRandom()
//
// // generate a random iv key
// // Half of the IV is random, the lower order bits are zeroed
// // such that the counter never wraps.
// // See https://github.com/matrix-org/matrix-ios-kit/blob/3dc0d8e46b4deb6669ed44f72ad79be56471354c/MatrixKit/Models/Room/MXEncryptedAttachments.m#L75
// val initVectorBytes = ByteArray(16) { 0.toByte() }
//
// val ivRandomPart = ByteArray(8)
// secureRandom.nextBytes(ivRandomPart)
//
// System.arraycopy(ivRandomPart, 0, initVectorBytes, 0, ivRandomPart.size)
//
// val key = ByteArray(32)
// secureRandom.nextBytes(key)
//
// val encryptCipher = Cipher.getInstance(CIPHER_ALGORITHM)
// val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM)
// val ivParameterSpec = IvParameterSpec(initVectorBytes)
// encryptCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec)
//
// val cipherInputStream = CipherInputStream(attachmentStream, encryptCipher)
//
// // Could it be possible to get the digest on the fly instead of
// val info = EncryptedFileInfo(
// url = null,
// mimetype = mimetype,
// key = EncryptedFileKey(
// alg = "A256CTR",
// ext = true,
// key_ops = listOf("encrypt", "decrypt"),
// kty = "oct",
// k = base64ToBase64Url(Base64.encodeToString(key, Base64.DEFAULT))
// ),
// iv = Base64.encodeToString(initVectorBytes, Base64.DEFAULT).replace("\n", "").replace("=", ""),
// //hashes = mapOf("sha256" to base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))),
// v = "v2"
// )
//
// val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM)
// return DigestInputStream(cipherInputStream, messageDigest) to info
// }
//
// fun updateInfoWithDigest(digestInputStream: DigestInputStream, info: EncryptedFileInfo): EncryptedFileInfo {
// return info.copy(
// hashes = mapOf("sha256" to base64ToUnpaddedBase64(Base64.encodeToString(digestInputStream.messageDigest.digest(), Base64.DEFAULT)))
// )
// }
/*** /***
* Encrypt an attachment stream. * Encrypt an attachment stream.
* DO NOT USE for big files, it will load all in memory
* @param attachmentStream the attachment stream. Will be closed after this method call. * @param attachmentStream the attachment stream. Will be closed after this method call.
* @param mimetype the mime type * @param mimetype the mime type
* @return the encryption file info * @return the encryption file info
@ -59,14 +175,14 @@ internal object MXEncryptedAttachments {
val key = ByteArray(32) val key = ByteArray(32)
secureRandom.nextBytes(key) secureRandom.nextBytes(key)
ByteArrayOutputStream().use { outputStream -> val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM)
val byteArrayOutputStream = ByteArrayOutputStream()
byteArrayOutputStream.use { outputStream ->
val encryptCipher = Cipher.getInstance(CIPHER_ALGORITHM) val encryptCipher = Cipher.getInstance(CIPHER_ALGORITHM)
val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM) val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM)
val ivParameterSpec = IvParameterSpec(initVectorBytes) val ivParameterSpec = IvParameterSpec(initVectorBytes)
encryptCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec) encryptCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec)
val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM)
val data = ByteArray(CRYPTO_BUFFER_SIZE) val data = ByteArray(CRYPTO_BUFFER_SIZE)
var read: Int var read: Int
var encodedBytes: ByteArray var encodedBytes: ByteArray
@ -85,26 +201,26 @@ internal object MXEncryptedAttachments {
encodedBytes = encryptCipher.doFinal() encodedBytes = encryptCipher.doFinal()
messageDigest.update(encodedBytes, 0, encodedBytes.size) messageDigest.update(encodedBytes, 0, encodedBytes.size)
outputStream.write(encodedBytes) outputStream.write(encodedBytes)
return EncryptionResult(
encryptedFileInfo = EncryptedFileInfo(
url = null,
mimetype = mimetype,
key = EncryptedFileKey(
alg = "A256CTR",
ext = true,
keyOps = listOf("encrypt", "decrypt"),
kty = "oct",
k = base64ToBase64Url(Base64.encodeToString(key, Base64.DEFAULT))
),
iv = Base64.encodeToString(initVectorBytes, Base64.DEFAULT).replace("\n", "").replace("=", ""),
hashes = mapOf("sha256" to base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))),
v = "v2"
),
encryptedByteArray = outputStream.toByteArray()
)
.also { Timber.v("Encrypt in ${System.currentTimeMillis() - t0}ms") }
} }
return EncryptionResult(
encryptedFileInfo = EncryptedFileInfo(
url = null,
mimetype = mimetype,
key = EncryptedFileKey(
alg = "A256CTR",
ext = true,
key_ops = listOf("encrypt", "decrypt"),
kty = "oct",
k = base64ToBase64Url(Base64.encodeToString(key, Base64.DEFAULT))
),
iv = Base64.encodeToString(initVectorBytes, Base64.DEFAULT).replace("\n", "").replace("=", ""),
hashes = mapOf("sha256" to base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))),
v = "v2"
),
encryptedByteArray = byteArrayOutputStream.toByteArray()
)
.also { Timber.v("Encrypt in ${System.currentTimeMillis() - t0}ms") }
} }
/** /**
@ -114,15 +230,23 @@ internal object MXEncryptedAttachments {
* @param encryptedFileInfo the encryption file info * @param encryptedFileInfo the encryption file info
* @return the decrypted attachment stream * @return the decrypted attachment stream
*/ */
fun decryptAttachment(attachmentStream: InputStream?, encryptedFileInfo: EncryptedFileInfo?): InputStream? { fun decryptAttachment(attachmentStream: InputStream?, elementToDecrypt: ElementToDecrypt): InputStream? {
if (encryptedFileInfo?.isValid() != true) { try {
Timber.e("## decryptAttachment() : some fields are not defined, or invalid key fields") val digestCheckInputStream = MatrixDigestCheckInputStream(attachmentStream, elementToDecrypt.sha256)
val key = Base64.decode(base64UrlToBase64(elementToDecrypt.k), Base64.DEFAULT)
val initVectorBytes = Base64.decode(elementToDecrypt.iv, Base64.DEFAULT)
val decryptCipher = Cipher.getInstance(CIPHER_ALGORITHM)
val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM)
val ivParameterSpec = IvParameterSpec(initVectorBytes)
decryptCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec)
return CipherInputStream(digestCheckInputStream, decryptCipher)
} catch (failure: Throwable) {
Timber.e(failure, "## decryptAttachment() : failed to create stream")
return null return null
} }
val elementToDecrypt = encryptedFileInfo.toElementToDecrypt()
return decryptAttachment(attachmentStream, elementToDecrypt)
} }
/** /**
@ -132,62 +256,59 @@ internal object MXEncryptedAttachments {
* @param elementToDecrypt the elementToDecrypt info * @param elementToDecrypt the elementToDecrypt info
* @return the decrypted attachment stream * @return the decrypted attachment stream
*/ */
fun decryptAttachment(attachmentStream: InputStream?, elementToDecrypt: ElementToDecrypt?): InputStream? { fun decryptAttachment(attachmentStream: InputStream?, elementToDecrypt: ElementToDecrypt?, outputStream: OutputStream): Boolean {
// sanity checks // sanity checks
if (null == attachmentStream || elementToDecrypt == null) { if (null == attachmentStream || elementToDecrypt == null) {
Timber.e("## decryptAttachment() : null stream") Timber.e("## decryptAttachment() : null stream")
return null return false
} }
val t0 = System.currentTimeMillis() val t0 = System.currentTimeMillis()
ByteArrayOutputStream().use { outputStream -> try {
try { val key = Base64.decode(base64UrlToBase64(elementToDecrypt.k), Base64.DEFAULT)
val key = Base64.decode(base64UrlToBase64(elementToDecrypt.k), Base64.DEFAULT) val initVectorBytes = Base64.decode(elementToDecrypt.iv, Base64.DEFAULT)
val initVectorBytes = Base64.decode(elementToDecrypt.iv, Base64.DEFAULT)
val decryptCipher = Cipher.getInstance(CIPHER_ALGORITHM) val decryptCipher = Cipher.getInstance(CIPHER_ALGORITHM)
val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM) val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM)
val ivParameterSpec = IvParameterSpec(initVectorBytes) val ivParameterSpec = IvParameterSpec(initVectorBytes)
decryptCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec) decryptCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec)
val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM) val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM)
var read: Int var read: Int
val data = ByteArray(CRYPTO_BUFFER_SIZE) val data = ByteArray(CRYPTO_BUFFER_SIZE)
var decodedBytes: ByteArray var decodedBytes: ByteArray
attachmentStream.use { inputStream -> attachmentStream.use { inputStream ->
read = inputStream.read(data)
while (read != -1) {
messageDigest.update(data, 0, read)
decodedBytes = decryptCipher.update(data, 0, read)
outputStream.write(decodedBytes)
read = inputStream.read(data) read = inputStream.read(data)
while (read != -1) {
messageDigest.update(data, 0, read)
decodedBytes = decryptCipher.update(data, 0, read)
outputStream.write(decodedBytes)
read = inputStream.read(data)
}
} }
// decrypt the last chunk
decodedBytes = decryptCipher.doFinal()
outputStream.write(decodedBytes)
val currentDigestValue = base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))
if (elementToDecrypt.sha256 != currentDigestValue) {
Timber.e("## decryptAttachment() : Digest value mismatch")
return null
}
return outputStream.toByteArray().inputStream()
.also { Timber.v("Decrypt in ${System.currentTimeMillis() - t0}ms") }
} catch (oom: OutOfMemoryError) {
Timber.e(oom, "## decryptAttachment() failed: OOM")
} catch (e: Exception) {
Timber.e(e, "## decryptAttachment() failed")
} }
// decrypt the last chunk
decodedBytes = decryptCipher.doFinal()
outputStream.write(decodedBytes)
val currentDigestValue = base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))
if (elementToDecrypt.sha256 != currentDigestValue) {
Timber.e("## decryptAttachment() : Digest value mismatch")
return false
}
return true.also { Timber.v("Decrypt in ${System.currentTimeMillis() - t0}ms") }
} catch (oom: OutOfMemoryError) {
Timber.e(oom, "## decryptAttachment() failed: OOM")
} catch (e: Exception) {
Timber.e(e, "## decryptAttachment() failed")
} }
return null return false
} }
/** /**
@ -206,7 +327,7 @@ internal object MXEncryptedAttachments {
.replace("=", "") .replace("=", "")
} }
private fun base64ToUnpaddedBase64(base64: String): String { internal fun base64ToUnpaddedBase64(base64: String): String {
return base64.replace("\n".toRegex(), "") return base64.replace("\n".toRegex(), "")
.replace("=", "") .replace("=", "")
} }

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.matrix.android.internal.crypto.attachments package org.matrix.android.sdk.internal.crypto.attachments
import android.util.Base64 import android.util.Base64
import java.io.FilterInputStream import java.io.FilterInputStream

View File

@ -35,6 +35,10 @@ internal class ProgressRequestBody(private val delegate: RequestBody,
return delegate.contentType() return delegate.contentType()
} }
override fun isOneShot() = delegate.isOneShot()
override fun isDuplex() = delegate.isDuplex()
override fun contentLength(): Long { override fun contentLength(): Long {
try { try {
return delegate.contentLength() return delegate.contentLength()

View File

@ -143,20 +143,22 @@ internal class DefaultFileService @Inject constructor(
Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${!source.exhausted()}") Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${!source.exhausted()}")
if (elementToDecrypt != null) { if (elementToDecrypt != null) {
Timber.v("## decrypt file") Timber.v("## FileService: decrypt file")
val decryptedStream = MXEncryptedAttachments.decryptAttachment(source.inputStream(), elementToDecrypt) val decryptSuccess = MXEncryptedAttachments.decryptAttachment(
source.inputStream(),
elementToDecrypt,
destFile.outputStream().buffered()
)
response.close() response.close()
if (decryptedStream == null) { if (!decryptSuccess) {
return@flatMap Try.Failure(IllegalStateException("Decryption error")) return@flatMap Try.Failure(IllegalStateException("Decryption error"))
} else {
decryptedStream.use {
writeToFile(decryptedStream, destFile)
}
} }
} else { } else {
writeToFile(source.inputStream(), destFile) writeToFile(source.inputStream(), destFile)
response.close() response.close()
} }
} else {
Timber.v("## FileService: cache hit for $url")
} }
Try.just(copyFile(destFile, downloadMode)) Try.just(copyFile(destFile, downloadMode))

View File

@ -74,8 +74,8 @@ internal class DefaultContentUploadStateTracker @Inject constructor() : ContentU
updateState(key, progressData) updateState(key, progressData)
} }
internal fun setEncrypting(key: String) { internal fun setEncrypting(key: String, current: Long, total: Long) {
val progressData = ContentUploadStateTracker.State.Encrypting val progressData = ContentUploadStateTracker.State.Encrypting(current, total)
updateState(key, progressData) updateState(key, progressData)
} }

View File

@ -23,12 +23,14 @@ import com.squareup.moshi.Moshi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import okio.BufferedSink
import okio.source
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.session.content.ContentUrlResolver import org.matrix.android.sdk.api.session.content.ContentUrlResolver
import org.matrix.android.sdk.internal.di.Authenticated import org.matrix.android.sdk.internal.di.Authenticated
@ -38,6 +40,7 @@ import org.matrix.android.sdk.internal.network.toFailure
import java.io.File import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.IOException import java.io.IOException
import java.io.InputStream
import javax.inject.Inject import javax.inject.Inject
internal class FileUploader @Inject constructor(@Authenticated internal class FileUploader @Inject constructor(@Authenticated
@ -54,7 +57,21 @@ internal class FileUploader @Inject constructor(@Authenticated
filename: String?, filename: String?,
mimeType: String?, mimeType: String?,
progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse { progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse {
val uploadBody = file.asRequestBody(mimeType?.toMediaTypeOrNull()) val uploadBody = object : RequestBody() {
override fun contentLength() = file.length()
// Disable okhttp auto resend for 'large files'
override fun isOneShot() = contentLength() == 0L || contentLength() >= 1_000_000
override fun contentType(): MediaType? {
return mimeType?.toMediaTypeOrNull()
}
override fun writeTo(sink: BufferedSink) {
file.source().use { sink.writeAll(it) }
}
}
return upload(uploadBody, filename, progressListener) return upload(uploadBody, filename, progressListener)
} }
@ -66,6 +83,28 @@ internal class FileUploader @Inject constructor(@Authenticated
return upload(uploadBody, filename, progressListener) return upload(uploadBody, filename, progressListener)
} }
suspend fun uploadInputStream(inputStream: InputStream,
filename: String?,
mimeType: String?,
progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse {
val length = inputStream.available().toLong()
val uploadBody = object : RequestBody() {
override fun contentLength() = length
// Disable okhttp auto resend for 'large files'
override fun isOneShot() = contentLength() == 0L || contentLength() >= 1_000_000
override fun contentType(): MediaType? {
return mimeType?.toMediaTypeOrNull()
}
override fun writeTo(sink: BufferedSink) {
inputStream.source().use { sink.writeAll(it) }
}
}
return upload(uploadBody, filename, progressListener)
}
suspend fun uploadFromUri(uri: Uri, suspend fun uploadFromUri(uri: Uri,
filename: String?, filename: String?,
mimeType: String?, mimeType: String?,
@ -73,10 +112,7 @@ internal class FileUploader @Inject constructor(@Authenticated
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
val inputStream = context.contentResolver.openInputStream(uri) ?: throw FileNotFoundException() val inputStream = context.contentResolver.openInputStream(uri) ?: throw FileNotFoundException()
inputStream.use { return uploadInputStream(inputStream, filename, mimeType, progressListener)
uploadByteArray(it.readBytes(), filename, mimeType, progressListener)
}
}
} }
private suspend fun upload(uploadBody: RequestBody, filename: String?, progressListener: ProgressRequestBody.Listener?): ContentUploadResponse { private suspend fun upload(uploadBody: RequestBody, filename: String?, progressListener: ProgressRequestBody.Listener?): ContentUploadResponse {
val urlBuilder = uploadUrl.toHttpUrlOrNull()?.newBuilder() ?: throw RuntimeException() val urlBuilder = uploadUrl.toHttpUrlOrNull()?.newBuilder() ?: throw RuntimeException()

View File

@ -24,6 +24,7 @@ import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import id.zelory.compressor.Compressor import id.zelory.compressor.Compressor
import id.zelory.compressor.constraint.default import id.zelory.compressor.constraint.default
import org.matrix.android.sdk.api.extensions.tryThis
import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.content.ContentAttachmentData
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toContent
@ -37,12 +38,15 @@ import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
import org.matrix.android.sdk.internal.network.ProgressRequestBody import org.matrix.android.sdk.internal.network.ProgressRequestBody
import org.matrix.android.sdk.internal.session.DefaultFileService import org.matrix.android.sdk.internal.session.DefaultFileService
import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker
import org.matrix.android.sdk.internal.session.room.send.MultipleEventSendingDispatcherWorker import org.matrix.android.sdk.internal.session.room.send.MultipleEventSendingDispatcherWorker
import org.matrix.android.sdk.internal.worker.SessionWorkerParams import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import org.matrix.android.sdk.internal.worker.getSessionComponent import org.matrix.android.sdk.internal.worker.getSessionComponent
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
import java.util.UUID import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
@ -71,6 +75,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
@Inject lateinit var fileUploader: FileUploader @Inject lateinit var fileUploader: FileUploader
@Inject lateinit var contentUploadStateTracker: DefaultContentUploadStateTracker @Inject lateinit var contentUploadStateTracker: DefaultContentUploadStateTracker
@Inject lateinit var fileService: DefaultFileService @Inject lateinit var fileService: DefaultFileService
@Inject lateinit var cancelSendTracker: CancelSendTracker
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
val params = WorkerParamsFactory.fromData<Params>(inputData) val params = WorkerParamsFactory.fromData<Params>(inputData)
@ -102,6 +107,13 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
var newImageAttributes: NewImageAttributes? = null var newImageAttributes: NewImageAttributes? = null
val allCancelled = params.events.all { cancelSendTracker.isCancelRequestedFor(it.eventId, it.roomId) }
if (allCancelled) {
// there is no point in uploading the image!
return Result.success(inputData)
.also { Timber.e("## Send: Work cancelled by user") }
}
try { try {
val inputStream = context.contentResolver.openInputStream(attachment.queryUri) val inputStream = context.contentResolver.openInputStream(attachment.queryUri)
?: return Result.success( ?: return Result.success(
@ -112,16 +124,16 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
) )
) )
inputStream.use { // inputStream.use {
var uploadedThumbnailUrl: String? = null var uploadedThumbnailUrl: String? = null
var uploadedThumbnailEncryptedFileInfo: EncryptedFileInfo? = null var uploadedThumbnailEncryptedFileInfo: EncryptedFileInfo? = null
ThumbnailExtractor.extractThumbnail(context, params.attachment)?.let { thumbnailData -> ThumbnailExtractor.extractThumbnail(context, params.attachment)?.let { thumbnailData ->
val thumbnailProgressListener = object : ProgressRequestBody.Listener { val thumbnailProgressListener = object : ProgressRequestBody.Listener {
override fun onProgress(current: Long, total: Long) { override fun onProgress(current: Long, total: Long) {
notifyTracker(params) { contentUploadStateTracker.setProgressThumbnail(it, current, total) } notifyTracker(params) { contentUploadStateTracker.setProgressThumbnail(it, current, total) }
}
} }
}
try { try {
val contentUploadResponse = if (params.isEncrypted) { val contentUploadResponse = if (params.isEncrypted) {
@ -140,27 +152,30 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
thumbnailProgressListener) thumbnailProgressListener)
} }
uploadedThumbnailUrl = contentUploadResponse.contentUri uploadedThumbnailUrl = contentUploadResponse.contentUri
} catch (t: Throwable) { } catch (t: Throwable) {
Timber.e(t, "Thumbnail update failed") Timber.e(t, "Thumbnail update failed")
}
} }
}
val progressListener = object : ProgressRequestBody.Listener { val progressListener = object : ProgressRequestBody.Listener {
override fun onProgress(current: Long, total: Long) { override fun onProgress(current: Long, total: Long) {
notifyTracker(params) { notifyTracker(params) {
if (isStopped) { if (isStopped) {
contentUploadStateTracker.setFailure(it, Throwable("Cancelled")) contentUploadStateTracker.setFailure(it, Throwable("Cancelled"))
} else { } else {
contentUploadStateTracker.setProgress(it, current, total) contentUploadStateTracker.setProgress(it, current, total)
}
} }
} }
} }
}
var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null
return try { return try {
var modifiedStream: InputStream
if (attachment.type == ContentAttachmentData.Type.IMAGE && params.compressBeforeSending) {
// Compressor library works with File instead of Uri for now. Since Scoped Storage doesn't allow us to access files directly, we should // Compressor library works with File instead of Uri for now. Since Scoped Storage doesn't allow us to access files directly, we should
// copy it to a cache folder by using InputStream and OutputStream. // copy it to a cache folder by using InputStream and OutputStream.
// https://github.com/zetbaitsu/Compressor/pull/150 // https://github.com/zetbaitsu/Compressor/pull/150
@ -178,58 +193,86 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
inputStream.copyTo(outputStream) inputStream.copyTo(outputStream)
} }
if (attachment.type == ContentAttachmentData.Type.IMAGE && params.compressBeforeSending) { val compressedFile = Compressor.compress(context, cacheFile) {
cacheFile = Compressor.compress(context, cacheFile) { default(
default( width = MAX_IMAGE_SIZE,
width = MAX_IMAGE_SIZE, height = MAX_IMAGE_SIZE
height = MAX_IMAGE_SIZE )
) }
}.also { compressedFile ->
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeFile(compressedFile.absolutePath, options) BitmapFactory.decodeFile(compressedFile.absolutePath, options)
val fileSize = compressedFile.length().toInt() val fileSize = compressedFile.length().toInt()
newImageAttributes = NewImageAttributes( newImageAttributes = NewImageAttributes(
options.outWidth, options.outWidth,
options.outHeight, options.outHeight,
fileSize fileSize
) )
modifiedStream = compressedFile.inputStream()
} else {
// Unfortunatly the original stream is not always able to provide content length
// by passing by a temp copy it's working (better experience for upload progress..)
modifiedStream = if (tryThis { inputStream.available() } ?: 0 <= 0) {
val tmp = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir)
tmp.outputStream().use {
inputStream.copyTo(it)
} }
} tmp.inputStream()
} else inputStream
}
val contentUploadResponse = if (params.isEncrypted) { val contentUploadResponse = if (params.isEncrypted) {
Timber.v("Encrypt file") Timber.v("## FileService: Encrypt file")
notifyTracker(params) { contentUploadStateTracker.setEncrypting(it) }
val encryptionResult = MXEncryptedAttachments.encryptAttachment(cacheFile.inputStream(), attachment.getSafeMimeType()) val tmpEncrypted = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir)
uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo
fileUploader uploadedFileEncryptedFileInfo =
.uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener) MXEncryptedAttachments.encrypt(modifiedStream, attachment.getSafeMimeType(), tmpEncrypted) { read, total ->
} else { notifyTracker(params) {
fileUploader contentUploadStateTracker.setEncrypting(it, read.toLong(), total.toLong())
.uploadFile(cacheFile, attachment.name, attachment.getSafeMimeType(), progressListener) }
} }
// If it's a file update the file service so that it does not redownload? Timber.v("## FileService: Uploading file")
if (params.attachment.type == ContentAttachmentData.Type.FILE) {
fileUploader
.uploadFile(tmpEncrypted, attachment.name, "application/octet-stream", progressListener)
.also {
// we can delete?
tryThis { tmpEncrypted.delete() }
}
} else {
Timber.v("## FileService: Clear file")
fileUploader
.uploadInputStream(modifiedStream, attachment.name, attachment.getSafeMimeType(), progressListener)
}
// If it's a file update the file service so that it does not redownload?
// if (params.attachment.type == ContentAttachmentData.Type.FILE) {
Timber.v("## FileService: Update cache storage for ${contentUploadResponse.contentUri}")
try {
context.contentResolver.openInputStream(attachment.queryUri)?.let { context.contentResolver.openInputStream(attachment.queryUri)?.let {
fileService.storeDataFor(contentUploadResponse.contentUri, params.attachment.getSafeMimeType(), it) fileService.storeDataFor(contentUploadResponse.contentUri, params.attachment.getSafeMimeType(), it)
} }
Timber.v("## FileService: cache storage updated")
} catch (failure: Throwable) {
Timber.e(failure, "## FileService: Failed to update fileservice cache")
} }
// }
handleSuccess(params, handleSuccess(params,
contentUploadResponse.contentUri, contentUploadResponse.contentUri,
uploadedFileEncryptedFileInfo, uploadedFileEncryptedFileInfo,
uploadedThumbnailUrl, uploadedThumbnailUrl,
uploadedThumbnailEncryptedFileInfo, uploadedThumbnailEncryptedFileInfo,
newImageAttributes) newImageAttributes)
} catch (t: Throwable) { } catch (t: Throwable) {
Timber.e(t) Timber.e(t, "## FileService: ERROR ${t.localizedMessage}")
handleFailure(params, t) handleFailure(params, t)
}
} }
// }
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e, "## FileService: ERROR")
notifyTracker(params) { contentUploadStateTracker.setFailure(it, e) } notifyTracker(params) { contentUploadStateTracker.setFailure(it, e) }
return Result.success( return Result.success(
WorkerParamsFactory.toData( WorkerParamsFactory.toData(
@ -259,7 +302,6 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
thumbnailUrl: String?, thumbnailUrl: String?,
thumbnailEncryptedFileInfo: EncryptedFileInfo?, thumbnailEncryptedFileInfo: EncryptedFileInfo?,
newImageAttributes: NewImageAttributes?): Result { newImageAttributes: NewImageAttributes?): Result {
Timber.v("handleSuccess $attachmentUrl, work is stopped $isStopped")
notifyTracker(params) { contentUploadStateTracker.setSuccess(it) } notifyTracker(params) { contentUploadStateTracker.setSuccess(it) }
val updatedEvents = params.events val updatedEvents = params.events
@ -268,7 +310,9 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
} }
val sendParams = MultipleEventSendingDispatcherWorker.Params(params.sessionId, updatedEvents, params.isEncrypted) val sendParams = MultipleEventSendingDispatcherWorker.Params(params.sessionId, updatedEvents, params.isEncrypted)
return Result.success(WorkerParamsFactory.toData(sendParams)) return Result.success(WorkerParamsFactory.toData(sendParams)).also {
Timber.v("## handleSuccess $attachmentUrl, work is stopped $isStopped")
}
} }
private fun updateEvent(event: Event, private fun updateEvent(event: Event,

View File

@ -61,19 +61,23 @@ internal class DefaultContentDownloadStateTracker @Inject constructor() : Progre
// private fun URL.toKey() = toString() // private fun URL.toKey() = toString()
override fun update(url: String, bytesRead: Long, contentLength: Long, done: Boolean) { override fun update(url: String, bytesRead: Long, contentLength: Long, done: Boolean) {
Timber.v("## DL Progress url:$url read:$bytesRead total:$contentLength done:$done") mainHandler.post {
if (done) { Timber.v("## DL Progress url:$url read:$bytesRead total:$contentLength done:$done")
updateState(url, ContentDownloadStateTracker.State.Success) if (done) {
} else { updateState(url, ContentDownloadStateTracker.State.Success)
updateState(url, ContentDownloadStateTracker.State.Downloading(bytesRead, contentLength, contentLength == -1L)) } else {
updateState(url, ContentDownloadStateTracker.State.Downloading(bytesRead, contentLength, contentLength == -1L))
}
} }
} }
override fun error(url: String, errorCode: Int) { override fun error(url: String, errorCode: Int) {
Timber.v("## DL Progress Error code:$errorCode") mainHandler.post {
updateState(url, ContentDownloadStateTracker.State.Failure(errorCode)) Timber.v("## DL Progress Error code:$errorCode")
listeners[url]?.forEach { updateState(url, ContentDownloadStateTracker.State.Failure(errorCode))
tryThis { it.onDownloadStateUpdate(ContentDownloadStateTracker.State.Failure(errorCode)) } listeners[url]?.forEach {
tryThis { it.onDownloadStateUpdate(ContentDownloadStateTracker.State.Failure(errorCode)) }
}
} }
} }

View File

@ -14,9 +14,9 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.matrix.android.internal.session.room.send package org.matrix.android.sdk.internal.session.room.send
import im.vector.matrix.android.internal.session.SessionScope import org.matrix.android.sdk.internal.session.SessionScope
import javax.inject.Inject import javax.inject.Inject
/** /**

View File

@ -17,6 +17,7 @@
package org.matrix.android.sdk.internal.session.room.send package org.matrix.android.sdk.internal.session.room.send
import android.net.Uri
import androidx.work.BackoffPolicy import androidx.work.BackoffPolicy
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest import androidx.work.OneTimeWorkRequest
@ -26,7 +27,6 @@ import com.squareup.inject.assisted.AssistedInject
import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.content.ContentAttachmentData
import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.isImageMessage
import org.matrix.android.sdk.api.session.events.model.isTextMessage import org.matrix.android.sdk.api.session.events.model.isTextMessage
import org.matrix.android.sdk.api.session.room.model.message.OptionItem import org.matrix.android.sdk.api.session.room.model.message.OptionItem
import org.matrix.android.sdk.api.session.room.send.SendService import org.matrix.android.sdk.api.session.room.send.SendService
@ -45,6 +45,15 @@ import org.matrix.android.sdk.internal.worker.AlwaysSuccessfulWorker
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import org.matrix.android.sdk.internal.worker.startChain import org.matrix.android.sdk.internal.worker.startChain
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent
import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -60,7 +69,8 @@ internal class DefaultSendService @AssistedInject constructor(
private val cryptoService: CryptoService, private val cryptoService: CryptoService,
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,
private val localEchoRepository: LocalEchoRepository, private val localEchoRepository: LocalEchoRepository,
private val roomEventSender: RoomEventSender private val roomEventSender: RoomEventSender,
private val cancelSendTracker: CancelSendTracker
) : SendService { ) : SendService {
@AssistedInject.Factory @AssistedInject.Factory
@ -136,36 +146,72 @@ internal class DefaultSendService @AssistedInject constructor(
} }
override fun resendMediaMessage(localEcho: TimelineEvent): Cancelable? { override fun resendMediaMessage(localEcho: TimelineEvent): Cancelable? {
if (localEcho.root.isImageMessage() && localEcho.root.sendState.hasFailed()) { if (localEcho.root.sendState.hasFailed()) {
// TODO this need a refactoring of attachement sending // TODO this need a refactoring of attachement sending
// val clearContent = localEcho.root.getClearContent() val clearContent = localEcho.root.getClearContent()
// val messageContent = clearContent?.toModel<MessageContent>() ?: return null val messageContent = clearContent?.toModel<MessageContent>() as? MessageWithAttachmentContent ?: return null
// when (messageContent.type) {
// MessageType.MSGTYPE_IMAGE -> { val url = messageContent.getFileUrl() ?: return null
// val imageContent = clearContent.toModel<MessageImageContent>() ?: return null if (url.startsWith("mxc://")) {
// val url = imageContent.url ?: return null // We need to resend only the message as the attachment is ok
// if (url.startsWith("mxc://")) { localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT)
// //TODO return sendEvent(localEcho.root)
// } else { }
// //The image has not yet been sent // we need to resend the media
// val attachmentData = ContentAttachmentData(
// size = imageContent.info!!.size.toLong(), when (messageContent) {
// mimeType = imageContent.info.mimeType!!, is MessageImageContent -> {
// width = imageContent.info.width.toLong(), // The image has not yet been sent
// height = imageContent.info.height.toLong(), val attachmentData = ContentAttachmentData(
// name = imageContent.body, size = messageContent.info!!.size.toLong(),
// path = imageContent.url, mimeType = messageContent.info.mimeType!!,
// type = ContentAttachmentData.Type.IMAGE width = messageContent.info.width.toLong(),
// ) height = messageContent.info.height.toLong(),
// monarchy.runTransactionSync { name = messageContent.body,
// EventEntity.where(it,eventId = localEcho.root.eventId ?: "").findFirst()?.let { queryUri = Uri.parse(messageContent.url),
// it.sendState = SendState.UNSENT type = ContentAttachmentData.Type.IMAGE
// } )
// } localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT)
// return internalSendMedia(localEcho.root,attachmentData) return internalSendMedia(listOf(localEcho.root), attachmentData, true)
// } }
// } is MessageVideoContent -> {
// } val attachmentData = ContentAttachmentData(
size = messageContent.videoInfo?.size ?: 0L,
mimeType = messageContent.mimeType,
width = messageContent.videoInfo?.width?.toLong(),
height = messageContent.videoInfo?.height?.toLong(),
duration = messageContent.videoInfo?.duration?.toLong(),
name = messageContent.body,
queryUri = Uri.parse(messageContent.url),
type = ContentAttachmentData.Type.VIDEO
)
localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT)
return internalSendMedia(listOf(localEcho.root), attachmentData, true)
}
is MessageFileContent -> {
val attachmentData = ContentAttachmentData(
size = messageContent.info!!.size,
mimeType = messageContent.info.mimeType!!,
name = messageContent.body,
queryUri = Uri.parse(messageContent.url),
type = ContentAttachmentData.Type.FILE
)
localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT)
return internalSendMedia(listOf(localEcho.root), attachmentData, true)
}
is MessageAudioContent -> {
val attachmentData = ContentAttachmentData(
size = messageContent.audioInfo?.size ?: 0,
duration = messageContent.audioInfo?.duration?.toLong() ?: 0L,
mimeType = messageContent.audioInfo?.mimeType,
name = messageContent.body,
queryUri = Uri.parse(messageContent.url),
type = ContentAttachmentData.Type.AUDIO
)
localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT)
return internalSendMedia(listOf(localEcho.root), attachmentData, true)
}
}
return null return null
} }
return null return null
@ -196,16 +242,34 @@ internal class DefaultSendService @AssistedInject constructor(
} }
} }
override fun cancelSend(eventId: String) {
cancelSendTracker.markLocalEchoForCancel(eventId, roomId)
taskExecutor.executorScope.launch {
localEchoRepository.deleteFailedEcho(roomId, eventId)
}
}
override fun resendAllFailedMessages() { override fun resendAllFailedMessages() {
taskExecutor.executorScope.launch { taskExecutor.executorScope.launch {
val eventsToResend = localEchoRepository.getAllFailedEventsToResend(roomId) val eventsToResend = localEchoRepository.getAllFailedEventsToResend(roomId)
eventsToResend.forEach { eventsToResend.forEach {
sendEvent(it) if (it.root.isTextMessage()) {
resendTextMessage(it)
} else if (it.root.isAttachmentMessage()) {
resendMediaMessage(it)
}
} }
localEchoRepository.updateSendState(roomId, eventsToResend.mapNotNull { it.eventId }, SendState.UNSENT) localEchoRepository.updateSendState(roomId, eventsToResend.map { it.eventId }, SendState.UNSENT)
} }
} }
// override fun failAllPendingMessages() {
// taskExecutor.executorScope.launch {
// val eventsToResend = localEchoRepository.getAllEventsWithStates(roomId, SendState.PENDING_STATES)
// localEchoRepository.updateSendState(roomId, eventsToResend.map { it.eventId }, SendState.UNDELIVERED)
// }
// }
override fun sendMedia(attachment: ContentAttachmentData, override fun sendMedia(attachment: ContentAttachmentData,
compressBeforeSending: Boolean, compressBeforeSending: Boolean,
roomIds: Set<String>): Cancelable { roomIds: Set<String>): Cancelable {

View File

@ -54,6 +54,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
@Inject lateinit var crypto: CryptoService @Inject lateinit var crypto: CryptoService
@Inject lateinit var localEchoRepository: LocalEchoRepository @Inject lateinit var localEchoRepository: LocalEchoRepository
@Inject lateinit var cancelSendTracker: CancelSendTracker
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
Timber.v("Start Encrypt work") Timber.v("Start Encrypt work")
@ -61,7 +62,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
?: return Result.success() ?: return Result.success()
.also { Timber.e("Unable to parse work parameters") } .also { Timber.e("Unable to parse work parameters") }
Timber.v("Start Encrypt work for event ${params.event.eventId}") Timber.v("## SendEvent: Start Encrypt work for event ${params.event.eventId}")
if (params.lastFailureMessage != null) { if (params.lastFailureMessage != null) {
// Transmit the error // Transmit the error
return Result.success(inputData) return Result.success(inputData)
@ -75,6 +76,12 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
if (localEvent.eventId == null) { if (localEvent.eventId == null) {
return Result.success() return Result.success()
} }
if (cancelSendTracker.isCancelRequestedFor(localEvent.eventId, localEvent.roomId)) {
return Result.success()
.also { Timber.e("## SendEvent: Event sending has been cancelled ${localEvent.eventId}") }
}
localEchoRepository.updateSendState(localEvent.eventId, SendState.ENCRYPTING) localEchoRepository.updateSendState(localEvent.eventId, SendState.ENCRYPTING)
val localMutableContent = localEvent.content?.toMutableMap() ?: mutableMapOf() val localMutableContent = localEvent.content?.toMutableMap() ?: mutableMapOf()

View File

@ -30,7 +30,6 @@ import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult
import org.matrix.android.sdk.internal.database.helper.nextId import org.matrix.android.sdk.internal.database.helper.nextId
import org.matrix.android.sdk.internal.database.mapper.ContentMapper import org.matrix.android.sdk.internal.database.mapper.ContentMapper
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.mapper.toEntity import org.matrix.android.sdk.internal.database.mapper.toEntity
import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.EventInsertEntity import org.matrix.android.sdk.internal.database.model.EventInsertEntity
@ -88,7 +87,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
} }
fun updateSendState(eventId: String, sendState: SendState) { fun updateSendState(eventId: String, sendState: SendState) {
Timber.v("Update local state of $eventId to ${sendState.name}") Timber.v("## SendEvent: [${System.currentTimeMillis()}] Update local state of $eventId to ${sendState.name}")
monarchy.writeAsync { realm -> monarchy.writeAsync { realm ->
val sendingEventEntity = EventEntity.where(realm, eventId).findFirst() val sendingEventEntity = EventEntity.where(realm, eventId).findFirst()
if (sendingEventEntity != null) { if (sendingEventEntity != null) {
@ -114,9 +113,13 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
} }
suspend fun deleteFailedEcho(roomId: String, localEcho: TimelineEvent) { suspend fun deleteFailedEcho(roomId: String, localEcho: TimelineEvent) {
deleteFailedEcho(roomId, localEcho.eventId)
}
suspend fun deleteFailedEcho(roomId: String, eventId: String?) {
monarchy.awaitTransaction { realm -> monarchy.awaitTransaction { realm ->
TimelineEventEntity.where(realm, roomId = roomId, eventId = localEcho.root.eventId ?: "").findFirst()?.deleteFromRealm() TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId ?: "").findFirst()?.deleteFromRealm()
EventEntity.where(realm, eventId = localEcho.root.eventId ?: "").findFirst()?.deleteFromRealm() EventEntity.where(realm, eventId = eventId ?: "").findFirst()?.deleteFromRealm()
roomSummaryUpdater.updateSendingInformation(realm, roomId) roomSummaryUpdater.updateSendingInformation(realm, roomId)
} }
} }
@ -142,45 +145,47 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
} }
} }
fun getAllFailedEventsToResend(roomId: String): List<Event> { fun getAllFailedEventsToResend(roomId: String): List<TimelineEvent> {
return getAllEventsWithStates(roomId, SendState.HAS_FAILED_STATES)
}
fun getAllEventsWithStates(roomId: String, states : List<SendState>): List<TimelineEvent> {
return Realm.getInstance(monarchy.realmConfiguration).use { realm -> return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
TimelineEventEntity TimelineEventEntity
.findAllInRoomWithSendStates(realm, roomId, SendState.HAS_FAILED_STATES) .findAllInRoomWithSendStates(realm, roomId, states)
.sortedByDescending { it.displayIndex } .sortedByDescending { it.displayIndex }
.mapNotNull { it.root?.asDomain() } .mapNotNull { it?.let { timelineEventMapper.map(it) } }
.filter { event -> .filter { event ->
when (event.getClearType()) { when (event.root.getClearType()) {
EventType.MESSAGE, EventType.MESSAGE,
EventType.REDACTION, EventType.REDACTION,
EventType.REACTION -> { EventType.REACTION -> {
val content = event.getClearContent().toModel<MessageContent>() val content = event.root.getClearContent().toModel<MessageContent>()
if (content != null) { if (content != null) {
when (content.msgType) { when (content.msgType) {
MessageType.MSGTYPE_EMOTE, MessageType.MSGTYPE_EMOTE,
MessageType.MSGTYPE_NOTICE, MessageType.MSGTYPE_NOTICE,
MessageType.MSGTYPE_LOCATION, MessageType.MSGTYPE_LOCATION,
MessageType.MSGTYPE_TEXT -> { MessageType.MSGTYPE_TEXT,
true
}
MessageType.MSGTYPE_FILE, MessageType.MSGTYPE_FILE,
MessageType.MSGTYPE_VIDEO, MessageType.MSGTYPE_VIDEO,
MessageType.MSGTYPE_IMAGE, MessageType.MSGTYPE_IMAGE,
MessageType.MSGTYPE_AUDIO -> { MessageType.MSGTYPE_AUDIO -> {
// need to resend the attachment // need to resend the attachment
false true
} }
else -> { else -> {
Timber.e("Cannot resend message ${event.type} / ${content.msgType}") Timber.e("Cannot resend message ${event.root.getClearType()} / ${content.msgType}")
false false
} }
} }
} else { } else {
Timber.e("Unsupported message to resend ${event.type}") Timber.e("Unsupported message to resend ${event.root.getClearType()}")
false false
} }
} }
else -> { else -> {
Timber.e("Unsupported message to resend ${event.type}") Timber.e("Unsupported message to resend ${event.root.getClearType()}")
false false
} }
} }

View File

@ -58,7 +58,7 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo
@Inject lateinit var localEchoRepository: LocalEchoRepository @Inject lateinit var localEchoRepository: LocalEchoRepository
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
Timber.v("Start dispatch sending multiple event work") Timber.v("## SendEvent: Start dispatch sending multiple event work")
val params = WorkerParamsFactory.fromData<Params>(inputData) val params = WorkerParamsFactory.fromData<Params>(inputData)
?: return Result.success() ?: return Result.success()
.also { Timber.e("Unable to parse work parameters") } .also { Timber.e("Unable to parse work parameters") }
@ -72,18 +72,21 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo
} }
// Transmit the error if needed? // Transmit the error if needed?
return Result.success(inputData) return Result.success(inputData)
.also { Timber.e("Work cancelled due to input error from parent") } .also { Timber.e("## SendEvent: Work cancelled due to input error from parent ${params.lastFailureMessage}") }
} }
// Create a work for every event // Create a work for every event
params.events.forEach { event -> params.events.forEach { event ->
if (params.isEncrypted) { if (params.isEncrypted) {
Timber.v("Send event in encrypted room") localEchoRepository.updateSendState(event.eventId ?: "", SendState.ENCRYPTING)
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule encrypt and send event ${event.eventId}")
val encryptWork = createEncryptEventWork(params.sessionId, event, true) val encryptWork = createEncryptEventWork(params.sessionId, event, true)
// Note that event will be replaced by the result of the previous work // Note that event will be replaced by the result of the previous work
val sendWork = createSendEventWork(params.sessionId, event, false) val sendWork = createSendEventWork(params.sessionId, event, false)
timelineSendEventWorkCommon.postSequentialWorks(event.roomId!!, encryptWork, sendWork) timelineSendEventWorkCommon.postSequentialWorks(event.roomId!!, encryptWork, sendWork)
} else { } else {
localEchoRepository.updateSendState(event.eventId ?: "", SendState.SENDING)
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule send event ${event.eventId}")
val sendWork = createSendEventWork(params.sessionId, event, true) val sendWork = createSendEventWork(params.sessionId, event, true)
timelineSendEventWorkCommon.postWork(event.roomId!!, sendWork) timelineSendEventWorkCommon.postWork(event.roomId!!, sendWork)
} }

View File

@ -39,13 +39,16 @@ internal class RoomEventSender @Inject constructor(
) { ) {
fun sendEvent(event: Event): Cancelable { fun sendEvent(event: Event): Cancelable {
// Encrypted room handling // Encrypted room handling
return if (cryptoService.isRoomEncrypted(event.roomId ?: "")) { return if (cryptoService.isRoomEncrypted(event.roomId ?: "")
Timber.v("Send event in encrypted room") && !event.isEncrypted() // In case of resend where it's already encrypted so skip to send
) {
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule encrypt and send event ${event.eventId}")
val encryptWork = createEncryptEventWork(event, true) val encryptWork = createEncryptEventWork(event, true)
// Note that event will be replaced by the result of the previous work // Note that event will be replaced by the result of the previous work
val sendWork = createSendEventWork(event, false) val sendWork = createSendEventWork(event, false)
timelineSendEventWorkCommon.postSequentialWorks(event.roomId ?: "", encryptWork, sendWork) timelineSendEventWorkCommon.postSequentialWorks(event.roomId ?: "", encryptWork, sendWork)
} else { } else {
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule send event ${event.eventId}")
val sendWork = createSendEventWork(event, true) val sendWork = createSendEventWork(event, true)
timelineSendEventWorkCommon.postWork(event.roomId ?: "", sendWork) timelineSendEventWorkCommon.postWork(event.roomId ?: "", sendWork)
} }

View File

@ -34,7 +34,7 @@ import org.matrix.android.sdk.internal.worker.getSessionComponent
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
private const val MAX_NUMBER_OF_RETRY_BEFORE_FAILING = 3 // private const val MAX_NUMBER_OF_RETRY_BEFORE_FAILING = 3
/** /**
* Possible previous worker: [EncryptEventWorker] or first worker * Possible previous worker: [EncryptEventWorker] or first worker
@ -56,12 +56,12 @@ internal class SendEventWorker(context: Context,
@Inject lateinit var localEchoRepository: LocalEchoRepository @Inject lateinit var localEchoRepository: LocalEchoRepository
@Inject lateinit var roomAPI: RoomAPI @Inject lateinit var roomAPI: RoomAPI
@Inject lateinit var eventBus: EventBus @Inject lateinit var eventBus: EventBus
@Inject lateinit var cancelSendTracker: CancelSendTracker
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
val params = WorkerParamsFactory.fromData<Params>(inputData) val params = WorkerParamsFactory.fromData<Params>(inputData)
?: return Result.success() ?: return Result.success()
.also { Timber.e("Unable to parse work parameters") } .also { Timber.e("## SendEvent: Unable to parse work parameters") }
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
sessionComponent.inject(this) sessionComponent.inject(this)
@ -75,22 +75,32 @@ internal class SendEventWorker(context: Context,
.also { Timber.e("Work cancelled due to bad input data") } .also { Timber.e("Work cancelled due to bad input data") }
} }
if (cancelSendTracker.isCancelRequestedFor(params.eventId, params.roomId)) {
return Result.success()
.also {
cancelSendTracker.markCancelled(params.eventId, params.roomId)
Timber.e("## SendEvent: Event sending has been cancelled ${params.eventId}")
}
}
if (params.lastFailureMessage != null) { if (params.lastFailureMessage != null) {
localEchoRepository.updateSendState(event.eventId, SendState.UNDELIVERED) localEchoRepository.updateSendState(event.eventId, SendState.UNDELIVERED)
// Transmit the error // Transmit the error
return Result.success(inputData) return Result.success(inputData)
.also { Timber.e("Work cancelled due to input error from parent") } .also { Timber.e("Work cancelled due to input error from parent") }
} }
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Send event ${params.eventId}")
return try { return try {
sendEvent(event.eventId, event.roomId, event.type, event.content) sendEvent(event.eventId, event.roomId, event.type, event.content)
Result.success() Result.success()
} catch (exception: Throwable) { } catch (exception: Throwable) {
// It does start from 0, we want it to stop if it fails the third time if (/*currentAttemptCount >= MAX_NUMBER_OF_RETRY_BEFORE_FAILING ||**/ !exception.shouldBeRetried()) {
val currentAttemptCount = runAttemptCount + 1 Timber.e("## SendEvent: [${System.currentTimeMillis()}] Send event Failed cannot retry ${params.eventId} > ${exception.localizedMessage}")
if (currentAttemptCount >= MAX_NUMBER_OF_RETRY_BEFORE_FAILING || !exception.shouldBeRetried()) { localEchoRepository.updateSendState(params.eventId, SendState.UNDELIVERED)
localEchoRepository.updateSendState(event.eventId, SendState.UNDELIVERED)
return Result.success() return Result.success()
} else { } else {
Timber.e("## SendEvent: [${System.currentTimeMillis()}] Send event Failed schedule retry ${params.eventId} > ${exception.localizedMessage}")
Result.retry() Result.retry()
} }
} }

View File

@ -115,6 +115,7 @@ internal class DefaultTimeline(
if (!results.isLoaded || !results.isValid) { if (!results.isLoaded || !results.isValid) {
return@OrderedRealmCollectionChangeListener return@OrderedRealmCollectionChangeListener
} }
Timber.v("## SendEvent: [${System.currentTimeMillis()}] DB update for room $roomId")
handleUpdates(results, changeSet) handleUpdates(results, changeSet)
} }

View File

@ -57,7 +57,7 @@ internal class TimelineSendEventWorkCommon @Inject constructor(
} }
} }
fun postWork(roomId: String, workRequest: OneTimeWorkRequest, policy: ExistingWorkPolicy = ExistingWorkPolicy.APPEND): Cancelable { fun postWork(roomId: String, workRequest: OneTimeWorkRequest, policy: ExistingWorkPolicy = ExistingWorkPolicy.APPEND_OR_REPLACE): Cancelable {
workManagerProvider.workManager workManagerProvider.workManager
.beginUniqueWork(buildWorkName(roomId), policy, workRequest) .beginUniqueWork(buildWorkName(roomId), policy, workRequest)
.enqueue() .enqueue()

View File

@ -26,8 +26,9 @@ import com.bumptech.glide.load.model.MultiModelLoaderFactory
import com.bumptech.glide.signature.ObjectKey import com.bumptech.glide.signature.ObjectKey
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.ImageContentRenderer
import org.matrix.android.sdk.api.Matrix
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.file.FileService
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException

View File

@ -34,6 +34,7 @@ import im.vector.app.core.glide.GlideApp
import im.vector.app.core.glide.GlideRequest import im.vector.app.core.glide.GlideRequest
import im.vector.app.core.glide.GlideRequests import im.vector.app.core.glide.GlideRequests
import im.vector.app.core.utils.getColorFromUserId import im.vector.app.core.utils.getColorFromUserId
import org.matrix.android.sdk.api.extensions.tryThis
import org.matrix.android.sdk.api.session.content.ContentUrlResolver import org.matrix.android.sdk.api.session.content.ContentUrlResolver
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
import javax.inject.Inject import javax.inject.Inject
@ -58,7 +59,7 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
fun clear(imageView: ImageView) { fun clear(imageView: ImageView) {
// It can be called after recycler view is destroyed, just silently catch // It can be called after recycler view is destroyed, just silently catch
tryThis { GlideApp.with(imageView).clear(imageView) } tryThis { GlideApp.with(imageView).clear(imageView) }
} }
@UiThread @UiThread

View File

@ -61,7 +61,7 @@ import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.isImageMessage import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage
import org.matrix.android.sdk.api.session.events.model.isTextMessage import org.matrix.android.sdk.api.session.events.model.isTextMessage
import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel

View File

@ -35,6 +35,7 @@ import im.vector.app.features.settings.VectorPreferences
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage
import org.matrix.android.sdk.api.session.events.model.isTextMessage import org.matrix.android.sdk.api.session.events.model.isTextMessage
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageContent

View File

@ -28,7 +28,6 @@ import im.vector.app.R
import im.vector.app.core.glide.GlideApp import im.vector.app.core.glide.GlideApp
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.ImageContentRenderer
import im.vector.matrix.android.api.session.room.send.SendState
@EpoxyModelClass(layout = R.layout.item_timeline_event_base) @EpoxyModelClass(layout = R.layout.item_timeline_event_base)
abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Holder>() { abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Holder>() {

View File

@ -39,6 +39,7 @@ import im.vector.app.core.utils.isLocalFile
import org.matrix.android.sdk.api.session.content.ContentUrlResolver import org.matrix.android.sdk.api.session.content.ContentUrlResolver
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import org.matrix.android.sdk.api.extensions.tryThis
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject