Merge pull request #2512 from vector-im/feature/bma/fix_span
Improve management of files
This commit is contained in:
commit
5541c2e190
@ -4,6 +4,7 @@ Changes in Element 1.0.12 (2020-XX-XX)
|
||||
Features ✨:
|
||||
- Add room aliases management, and room directory visibility management in a dedicated screen (#1579, #2428)
|
||||
- Room setting: update join rules and guest access (#2442)
|
||||
- Store encrypted file in cache and cleanup decrypted file at each app start (#2512)
|
||||
- Emoji Keyboard (#2520)
|
||||
|
||||
Improvements 🙌:
|
||||
@ -19,17 +20,19 @@ Translations 🗣:
|
||||
-
|
||||
|
||||
SDK API changes ⚠️:
|
||||
-
|
||||
- FileService: remove useless FileService.DownloadMode
|
||||
|
||||
Build 🧱:
|
||||
- Upgrade some dependencies and Kotlin version
|
||||
- Use fragment-ktx and preference-ktx dependencies (fix lint issue KtxExtensionAvailable)
|
||||
- Upgrade Realm dependency to 10.1.2
|
||||
|
||||
Test:
|
||||
-
|
||||
|
||||
Other changes:
|
||||
- Remove "Status.im" theme #2424
|
||||
- Log HTTP requests and responses in production (level BASIC, i.e. without any private data)
|
||||
|
||||
Changes in Element 1.0.11 (2020-11-27)
|
||||
===================================================
|
||||
|
@ -18,7 +18,7 @@ org.gradle.jvmargs=-Xmx2048m
|
||||
org.gradle.vfs.watch=true
|
||||
|
||||
vector.debugPrivateData=false
|
||||
vector.httpLogLevel=NONE
|
||||
vector.httpLogLevel=BASIC
|
||||
|
||||
# Note: to debug, you can put and uncomment the following lines in the file ~/.gradle/gradle.properties to override the value above
|
||||
#vector.debugPrivateData=true
|
||||
|
@ -9,7 +9,7 @@ buildscript {
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath "io.realm:realm-gradle-plugin:10.0.0"
|
||||
classpath "io.realm:realm-gradle-plugin:10.1.2"
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,7 +63,7 @@ android {
|
||||
|
||||
release {
|
||||
buildConfigField "boolean", "LOG_PRIVATE_DATA", "false"
|
||||
buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.NONE"
|
||||
buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.BASIC"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,6 @@
|
||||
package org.matrix.android.sdk.internal.network.interceptors
|
||||
|
||||
import androidx.annotation.NonNull
|
||||
import org.matrix.android.sdk.BuildConfig
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
@ -38,31 +37,28 @@ class FormattedJsonHttpLogger : HttpLoggingInterceptor.Logger {
|
||||
*/
|
||||
@Synchronized
|
||||
override fun log(@NonNull message: String) {
|
||||
// In RELEASE there is no log, but for sure, test again BuildConfig.DEBUG
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.v(message)
|
||||
Timber.v(message)
|
||||
|
||||
if (message.startsWith("{")) {
|
||||
// JSON Detected
|
||||
try {
|
||||
val o = JSONObject(message)
|
||||
logJson(o.toString(INDENT_SPACE))
|
||||
} catch (e: JSONException) {
|
||||
// Finally this is not a JSON string...
|
||||
Timber.e(e)
|
||||
}
|
||||
} else if (message.startsWith("[")) {
|
||||
// JSON Array detected
|
||||
try {
|
||||
val o = JSONArray(message)
|
||||
logJson(o.toString(INDENT_SPACE))
|
||||
} catch (e: JSONException) {
|
||||
// Finally not JSON...
|
||||
Timber.e(e)
|
||||
}
|
||||
if (message.startsWith("{")) {
|
||||
// JSON Detected
|
||||
try {
|
||||
val o = JSONObject(message)
|
||||
logJson(o.toString(INDENT_SPACE))
|
||||
} catch (e: JSONException) {
|
||||
// Finally this is not a JSON string...
|
||||
Timber.e(e)
|
||||
}
|
||||
} else if (message.startsWith("[")) {
|
||||
// JSON Array detected
|
||||
try {
|
||||
val o = JSONArray(message)
|
||||
logJson(o.toString(INDENT_SPACE))
|
||||
} catch (e: JSONException) {
|
||||
// Finally not JSON...
|
||||
Timber.e(e)
|
||||
}
|
||||
// Else not a json string to log
|
||||
}
|
||||
// Else not a json string to log
|
||||
}
|
||||
|
||||
private fun logJson(formattedJson: String) {
|
||||
|
@ -18,8 +18,12 @@ package org.matrix.android.sdk.api.session.file
|
||||
|
||||
import android.net.Uri
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.getFileName
|
||||
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
|
||||
import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
@ -27,23 +31,6 @@ import java.io.File
|
||||
*/
|
||||
interface FileService {
|
||||
|
||||
enum class DownloadMode {
|
||||
/**
|
||||
* Download file in external storage
|
||||
*/
|
||||
TO_EXPORT,
|
||||
|
||||
/**
|
||||
* Download file in cache
|
||||
*/
|
||||
FOR_INTERNAL_USE,
|
||||
|
||||
/**
|
||||
* Download file in file provider path
|
||||
*/
|
||||
FOR_EXTERNAL_SHARE
|
||||
}
|
||||
|
||||
enum class FileState {
|
||||
IN_CACHE,
|
||||
DOWNLOADING,
|
||||
@ -54,34 +41,79 @@ interface FileService {
|
||||
* Download a file.
|
||||
* Result will be a decrypted file, stored in the cache folder. url parameter will be used to create unique filename to avoid name collision.
|
||||
*/
|
||||
fun downloadFile(
|
||||
downloadMode: DownloadMode,
|
||||
id: String,
|
||||
fileName: String,
|
||||
mimeType: String?,
|
||||
url: String?,
|
||||
elementToDecrypt: ElementToDecrypt?,
|
||||
callback: MatrixCallback<File>): Cancelable
|
||||
fun downloadFile(fileName: String,
|
||||
mimeType: String?,
|
||||
url: String?,
|
||||
elementToDecrypt: ElementToDecrypt?,
|
||||
callback: MatrixCallback<File>): Cancelable
|
||||
|
||||
fun isFileInCache(mxcUrl: String, mimeType: String?): Boolean
|
||||
fun downloadFile(messageContent: MessageWithAttachmentContent,
|
||||
callback: MatrixCallback<File>): Cancelable =
|
||||
downloadFile(
|
||||
fileName = messageContent.getFileName(),
|
||||
mimeType = messageContent.mimeType,
|
||||
url = messageContent.getFileUrl(),
|
||||
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(),
|
||||
callback = callback
|
||||
)
|
||||
|
||||
fun isFileInCache(mxcUrl: String?,
|
||||
fileName: String,
|
||||
mimeType: String?,
|
||||
elementToDecrypt: ElementToDecrypt?
|
||||
): Boolean
|
||||
|
||||
fun isFileInCache(messageContent: MessageWithAttachmentContent) =
|
||||
isFileInCache(
|
||||
mxcUrl = messageContent.getFileUrl(),
|
||||
fileName = messageContent.getFileName(),
|
||||
mimeType = messageContent.mimeType,
|
||||
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt())
|
||||
|
||||
/**
|
||||
* Use this URI and pass it to intent using flag Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
* (if not other app won't be able to access it)
|
||||
*/
|
||||
fun getTemporarySharableURI(mxcUrl: String, mimeType: String?): Uri?
|
||||
fun getTemporarySharableURI(mxcUrl: String?,
|
||||
fileName: String,
|
||||
mimeType: String?,
|
||||
elementToDecrypt: ElementToDecrypt?): Uri?
|
||||
|
||||
fun getTemporarySharableURI(messageContent: MessageWithAttachmentContent): Uri? =
|
||||
getTemporarySharableURI(
|
||||
mxcUrl = messageContent.getFileUrl(),
|
||||
fileName = messageContent.getFileName(),
|
||||
mimeType = messageContent.mimeType,
|
||||
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt()
|
||||
)
|
||||
|
||||
/**
|
||||
* Get information on the given file.
|
||||
* Mimetype should be the same one as passed to downloadFile (limitation for now)
|
||||
*/
|
||||
fun fileState(mxcUrl: String, mimeType: String?): FileState
|
||||
fun fileState(mxcUrl: String?,
|
||||
fileName: String,
|
||||
mimeType: String?,
|
||||
elementToDecrypt: ElementToDecrypt?): FileState
|
||||
|
||||
fun fileState(messageContent: MessageWithAttachmentContent): FileState =
|
||||
fileState(
|
||||
mxcUrl = messageContent.getFileUrl(),
|
||||
fileName = messageContent.getFileName(),
|
||||
mimeType = messageContent.mimeType,
|
||||
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt()
|
||||
)
|
||||
|
||||
/**
|
||||
* Clears all the files downloaded by the service
|
||||
* Clears all the files downloaded by the service, including decrypted files
|
||||
*/
|
||||
fun clearCache()
|
||||
|
||||
/**
|
||||
* Clears all the decrypted files by the service
|
||||
*/
|
||||
fun clearDecryptedCache()
|
||||
|
||||
/**
|
||||
* Get size of cached files
|
||||
*/
|
||||
|
@ -71,9 +71,6 @@ internal interface MatrixComponent {
|
||||
@CacheDirectory
|
||||
fun cacheDir(): File
|
||||
|
||||
@ExternalFilesDirectory
|
||||
fun externalFilesDir(): File?
|
||||
|
||||
fun olmManager(): OlmManager
|
||||
|
||||
fun taskExecutor(): TaskExecutor
|
||||
|
@ -57,13 +57,6 @@ internal object MatrixModule {
|
||||
return context.cacheDir
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@ExternalFilesDirectory
|
||||
fun providesExternalFilesDir(context: Context): File? {
|
||||
return context.getExternalFilesDir(null)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@MatrixScope
|
||||
|
@ -16,14 +16,15 @@
|
||||
|
||||
package org.matrix.android.sdk.internal.network
|
||||
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.failure.shouldBeRetried
|
||||
import org.matrix.android.sdk.internal.network.ssl.CertUtil
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.delay
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.failure.shouldBeRetried
|
||||
import org.matrix.android.sdk.internal.network.ssl.CertUtil
|
||||
import retrofit2.Call
|
||||
import retrofit2.awaitResponse
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
|
||||
internal suspend inline fun <DATA : Any> executeRequest(eventBus: EventBus?,
|
||||
@ -49,6 +50,9 @@ internal class Request<DATA : Any>(private val eventBus: EventBus?) {
|
||||
throw response.toFailure(eventBus)
|
||||
}
|
||||
} catch (exception: Throwable) {
|
||||
// Log some details about the request which has failed
|
||||
Timber.e("Exception when executing request ${apiCall.request().method} ${apiCall.request().url.toString().substringBefore("?")}")
|
||||
|
||||
// Check if this is a certificateException
|
||||
CertUtil.getCertificateException(exception)
|
||||
// TODO Support certificate error once logged
|
||||
|
@ -21,6 +21,10 @@ import android.net.Uri
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.core.content.FileProvider
|
||||
import arrow.core.Try
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
|
||||
@ -29,35 +33,21 @@ import org.matrix.android.sdk.api.util.Cancelable
|
||||
import org.matrix.android.sdk.api.util.NoOpCancellable
|
||||
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
|
||||
import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments
|
||||
import org.matrix.android.sdk.internal.di.CacheDirectory
|
||||
import org.matrix.android.sdk.internal.di.ExternalFilesDirectory
|
||||
import org.matrix.android.sdk.internal.di.SessionDownloadsDirectory
|
||||
import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificateWithProgress
|
||||
import org.matrix.android.sdk.internal.session.download.DownloadProgressInterceptor.Companion.DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER
|
||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
||||
import org.matrix.android.sdk.internal.util.md5
|
||||
import org.matrix.android.sdk.internal.util.toCancelable
|
||||
import org.matrix.android.sdk.internal.util.writeToFile
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import okio.source
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.net.URLEncoder
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultFileService @Inject constructor(
|
||||
private val context: Context,
|
||||
@CacheDirectory
|
||||
private val cacheDirectory: File,
|
||||
@ExternalFilesDirectory
|
||||
private val externalFilesDirectory: File?,
|
||||
@SessionDownloadsDirectory
|
||||
private val sessionCacheDirectory: File,
|
||||
private val contentUrlResolver: ContentUrlResolver,
|
||||
@ -67,9 +57,17 @@ internal class DefaultFileService @Inject constructor(
|
||||
private val taskExecutor: TaskExecutor
|
||||
) : FileService {
|
||||
|
||||
private fun String.safeFileName() = URLEncoder.encode(this, Charsets.US_ASCII.displayName())
|
||||
// Legacy folder, will be deleted
|
||||
private val legacyFolder = File(sessionCacheDirectory, "MF")
|
||||
// Folder to store downloaded files (not decrypted)
|
||||
private val downloadFolder = File(sessionCacheDirectory, "F")
|
||||
// Folder to store decrypted files
|
||||
private val decryptedFolder = File(downloadFolder, "D")
|
||||
|
||||
private val downloadFolder = File(sessionCacheDirectory, "MF")
|
||||
init {
|
||||
// Clear the legacy downloaded files
|
||||
legacyFolder.deleteRecursively()
|
||||
}
|
||||
|
||||
/**
|
||||
* Retain ongoing downloads to avoid re-downloading and already downloading file
|
||||
@ -81,28 +79,26 @@ internal class DefaultFileService @Inject constructor(
|
||||
* Download file in the cache folder, and eventually decrypt it
|
||||
* TODO looks like files are copied 3 times
|
||||
*/
|
||||
override fun downloadFile(downloadMode: FileService.DownloadMode,
|
||||
id: String,
|
||||
fileName: String,
|
||||
override fun downloadFile(fileName: String,
|
||||
mimeType: String?,
|
||||
url: String?,
|
||||
elementToDecrypt: ElementToDecrypt?,
|
||||
callback: MatrixCallback<File>): Cancelable {
|
||||
val unwrappedUrl = url ?: return NoOpCancellable.also {
|
||||
url ?: return NoOpCancellable.also {
|
||||
callback.onFailure(IllegalArgumentException("url is null"))
|
||||
}
|
||||
|
||||
Timber.v("## FileService downloadFile $unwrappedUrl")
|
||||
Timber.v("## FileService downloadFile $url")
|
||||
|
||||
synchronized(ongoing) {
|
||||
val existing = ongoing[unwrappedUrl]
|
||||
val existing = ongoing[url]
|
||||
if (existing != null) {
|
||||
Timber.v("## FileService downloadFile is already downloading.. ")
|
||||
existing.add(callback)
|
||||
return NoOpCancellable
|
||||
} else {
|
||||
// mark as tracked
|
||||
ongoing[unwrappedUrl] = ArrayList()
|
||||
ongoing[url] = ArrayList()
|
||||
// and proceed to download
|
||||
}
|
||||
}
|
||||
@ -110,15 +106,15 @@ internal class DefaultFileService @Inject constructor(
|
||||
return taskExecutor.executorScope.launch(coroutineDispatchers.main) {
|
||||
withContext(coroutineDispatchers.io) {
|
||||
Try {
|
||||
if (!downloadFolder.exists()) {
|
||||
downloadFolder.mkdirs()
|
||||
if (!decryptedFolder.exists()) {
|
||||
decryptedFolder.mkdirs()
|
||||
}
|
||||
// ensure we use unique file name by using URL (mapped to suitable file name)
|
||||
// Also we need to add extension for the FileProvider, if not it lot's of app that it's
|
||||
// shared with will not function well (even if mime type is passed in the intent)
|
||||
File(downloadFolder, fileForUrl(unwrappedUrl, mimeType))
|
||||
}.flatMap { destFile ->
|
||||
if (!destFile.exists()) {
|
||||
getFiles(url, fileName, mimeType, elementToDecrypt != null)
|
||||
}.flatMap { cachedFiles ->
|
||||
if (!cachedFiles.file.exists()) {
|
||||
val resolvedUrl = contentUrlResolver.resolveFullSize(url) ?: return@flatMap Try.Failure(IllegalArgumentException("url is null"))
|
||||
|
||||
val request = Request.Builder()
|
||||
@ -141,79 +137,153 @@ internal class DefaultFileService @Inject constructor(
|
||||
|
||||
Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${!source.exhausted()}")
|
||||
|
||||
if (elementToDecrypt != null) {
|
||||
Timber.v("## FileService: decrypt file")
|
||||
val decryptSuccess = destFile.outputStream().buffered().use {
|
||||
MXEncryptedAttachments.decryptAttachment(
|
||||
source.inputStream(),
|
||||
elementToDecrypt,
|
||||
it
|
||||
)
|
||||
}
|
||||
response.close()
|
||||
if (!decryptSuccess) {
|
||||
return@flatMap Try.Failure(IllegalStateException("Decryption error"))
|
||||
}
|
||||
} else {
|
||||
writeToFile(source.inputStream(), destFile)
|
||||
response.close()
|
||||
}
|
||||
// Write the file to cache (encrypted version if the file is encrypted)
|
||||
writeToFile(source.inputStream(), cachedFiles.file)
|
||||
response.close()
|
||||
} else {
|
||||
Timber.v("## FileService: cache hit for $url")
|
||||
}
|
||||
|
||||
Try.just(copyFile(destFile, downloadMode))
|
||||
Try.just(cachedFiles)
|
||||
}
|
||||
}.fold({
|
||||
callback.onFailure(it)
|
||||
// notify concurrent requests
|
||||
val toNotify = synchronized(ongoing) {
|
||||
ongoing[unwrappedUrl]?.also {
|
||||
ongoing.remove(unwrappedUrl)
|
||||
}.flatMap { cachedFiles ->
|
||||
// Decrypt if necessary
|
||||
if (cachedFiles.decryptedFile != null) {
|
||||
if (!cachedFiles.decryptedFile.exists()) {
|
||||
Timber.v("## FileService: decrypt file")
|
||||
// Ensure the parent folder exists
|
||||
cachedFiles.decryptedFile.parentFile?.mkdirs()
|
||||
val decryptSuccess = cachedFiles.file.inputStream().use { inputStream ->
|
||||
cachedFiles.decryptedFile.outputStream().buffered().use { outputStream ->
|
||||
MXEncryptedAttachments.decryptAttachment(
|
||||
inputStream,
|
||||
elementToDecrypt,
|
||||
outputStream
|
||||
)
|
||||
}
|
||||
}
|
||||
if (!decryptSuccess) {
|
||||
return@flatMap Try.Failure(IllegalStateException("Decryption error"))
|
||||
}
|
||||
} else {
|
||||
Timber.v("## FileService: cache hit for decrypted file")
|
||||
}
|
||||
Try.just(cachedFiles.decryptedFile)
|
||||
} else {
|
||||
// Clear file
|
||||
Try.just(cachedFiles.file)
|
||||
}
|
||||
toNotify?.forEach { otherCallbacks ->
|
||||
tryOrNull { otherCallbacks.onFailure(it) }
|
||||
}
|
||||
}, { file ->
|
||||
callback.onSuccess(file)
|
||||
// notify concurrent requests
|
||||
val toNotify = synchronized(ongoing) {
|
||||
ongoing[unwrappedUrl]?.also {
|
||||
ongoing.remove(unwrappedUrl)
|
||||
}.fold(
|
||||
{ throwable ->
|
||||
callback.onFailure(throwable)
|
||||
// notify concurrent requests
|
||||
val toNotify = synchronized(ongoing) {
|
||||
ongoing[url]?.also {
|
||||
ongoing.remove(url)
|
||||
}
|
||||
}
|
||||
toNotify?.forEach { otherCallbacks ->
|
||||
tryOrNull { otherCallbacks.onFailure(throwable) }
|
||||
}
|
||||
},
|
||||
{ file ->
|
||||
callback.onSuccess(file)
|
||||
// notify concurrent requests
|
||||
val toNotify = synchronized(ongoing) {
|
||||
ongoing[url]?.also {
|
||||
ongoing.remove(url)
|
||||
}
|
||||
}
|
||||
Timber.v("## FileService additional to notify ${toNotify?.size ?: 0} ")
|
||||
toNotify?.forEach { otherCallbacks ->
|
||||
tryOrNull { otherCallbacks.onSuccess(file) }
|
||||
}
|
||||
}
|
||||
}
|
||||
Timber.v("## FileService additional to notify ${toNotify?.size ?: 0} ")
|
||||
toNotify?.forEach { otherCallbacks ->
|
||||
tryOrNull { otherCallbacks.onSuccess(file) }
|
||||
}
|
||||
})
|
||||
)
|
||||
}.toCancelable()
|
||||
}
|
||||
|
||||
fun storeDataFor(url: String, mimeType: String?, inputStream: InputStream) {
|
||||
val file = File(downloadFolder, fileForUrl(url, mimeType))
|
||||
val source = inputStream.source().buffer()
|
||||
file.sink().buffer().let { sink ->
|
||||
source.use { input ->
|
||||
sink.use { output ->
|
||||
output.writeAll(input)
|
||||
fun storeDataFor(mxcUrl: String,
|
||||
filename: String?,
|
||||
mimeType: String?,
|
||||
originalFile: File,
|
||||
encryptedFile: File?) {
|
||||
val files = getFiles(mxcUrl, filename, mimeType, encryptedFile != null)
|
||||
if (encryptedFile != null) {
|
||||
// We switch the two files here, original file it the decrypted file
|
||||
files.decryptedFile?.let { originalFile.copyTo(it) }
|
||||
encryptedFile.copyTo(files.file)
|
||||
} else {
|
||||
// Just copy the original file
|
||||
originalFile.copyTo(files.file)
|
||||
}
|
||||
}
|
||||
|
||||
private fun safeFileName(fileName: String?, mimeType: String?): String {
|
||||
return buildString {
|
||||
// filename has to be safe for the Android System
|
||||
val result = fileName
|
||||
?.replace("[^a-z A-Z0-9\\\\.\\-]".toRegex(), "_")
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
?: DEFAULT_FILENAME
|
||||
append(result)
|
||||
// Check that the extension is correct regarding the mimeType
|
||||
val extensionFromMime = mimeType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) }
|
||||
if (extensionFromMime != null) {
|
||||
// Compare
|
||||
val fileExtension = result.substringAfterLast(delimiter = ".", missingDelimiterValue = "")
|
||||
if (fileExtension.isEmpty() || fileExtension != extensionFromMime) {
|
||||
// Missing extension, or diff in extension, add the one provided by the mimetype
|
||||
append(".")
|
||||
append(extensionFromMime)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fileForUrl(url: String, mimeType: String?): String {
|
||||
val extension = mimeType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) }
|
||||
return if (extension != null) "${url.safeFileName()}.$extension" else url.safeFileName()
|
||||
override fun isFileInCache(mxcUrl: String?,
|
||||
fileName: String,
|
||||
mimeType: String?,
|
||||
elementToDecrypt: ElementToDecrypt?): Boolean {
|
||||
return fileState(mxcUrl, fileName, mimeType, elementToDecrypt) == FileService.FileState.IN_CACHE
|
||||
}
|
||||
|
||||
override fun isFileInCache(mxcUrl: String, mimeType: String?): Boolean {
|
||||
return File(downloadFolder, fileForUrl(mxcUrl, mimeType)).exists()
|
||||
internal data class CachedFiles(
|
||||
// This is the downloaded file. Can be clear or encrypted
|
||||
val file: File,
|
||||
// This is the decrypted file. Null if the original file is not encrypted
|
||||
val decryptedFile: File?
|
||||
) {
|
||||
fun getClearFile(): File = decryptedFile ?: file
|
||||
}
|
||||
|
||||
override fun fileState(mxcUrl: String, mimeType: String?): FileService.FileState {
|
||||
if (isFileInCache(mxcUrl, mimeType)) return FileService.FileState.IN_CACHE
|
||||
private fun getFiles(mxcUrl: String,
|
||||
fileName: String?,
|
||||
mimeType: String?,
|
||||
isEncrypted: Boolean): CachedFiles {
|
||||
val hashFolder = mxcUrl.md5()
|
||||
val safeFileName = safeFileName(fileName, mimeType)
|
||||
return if (isEncrypted) {
|
||||
// Encrypted file
|
||||
CachedFiles(
|
||||
File(downloadFolder, "$hashFolder/$ENCRYPTED_FILENAME"),
|
||||
File(decryptedFolder, "$hashFolder/$safeFileName")
|
||||
)
|
||||
} else {
|
||||
// Clear file
|
||||
CachedFiles(
|
||||
File(downloadFolder, "$hashFolder/$safeFileName"),
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun fileState(mxcUrl: String?,
|
||||
fileName: String,
|
||||
mimeType: String?,
|
||||
elementToDecrypt: ElementToDecrypt?): FileService.FileState {
|
||||
mxcUrl ?: return FileService.FileState.UNKNOWN
|
||||
if (getFiles(mxcUrl, fileName, mimeType, elementToDecrypt != null).file.exists()) return FileService.FileState.IN_CACHE
|
||||
val isDownloading = synchronized(ongoing) {
|
||||
ongoing[mxcUrl] != null
|
||||
}
|
||||
@ -224,26 +294,18 @@ internal class DefaultFileService @Inject constructor(
|
||||
* Use this URI and pass it to intent using flag Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
* (if not other app won't be able to access it)
|
||||
*/
|
||||
override fun getTemporarySharableURI(mxcUrl: String, mimeType: String?): Uri? {
|
||||
override fun getTemporarySharableURI(mxcUrl: String?,
|
||||
fileName: String,
|
||||
mimeType: String?,
|
||||
elementToDecrypt: ElementToDecrypt?): Uri? {
|
||||
mxcUrl ?: return null
|
||||
// this string could be extracted no?
|
||||
val authority = "${context.packageName}.mx-sdk.fileprovider"
|
||||
val targetFile = File(downloadFolder, fileForUrl(mxcUrl, mimeType))
|
||||
val targetFile = getFiles(mxcUrl, fileName, mimeType, elementToDecrypt != null).getClearFile()
|
||||
if (!targetFile.exists()) return null
|
||||
return FileProvider.getUriForFile(context, authority, targetFile)
|
||||
}
|
||||
|
||||
private fun copyFile(file: File, downloadMode: FileService.DownloadMode): File {
|
||||
// TODO some of this seems outdated, will need to be re-worked
|
||||
return when (downloadMode) {
|
||||
FileService.DownloadMode.TO_EXPORT ->
|
||||
file.copyTo(File(externalFilesDirectory, file.name), true)
|
||||
FileService.DownloadMode.FOR_EXTERNAL_SHARE ->
|
||||
file.copyTo(File(File(cacheDirectory, "ext_share"), file.name), true)
|
||||
FileService.DownloadMode.FOR_INTERNAL_USE ->
|
||||
file
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCacheSize(): Int {
|
||||
return downloadFolder.walkTopDown()
|
||||
.onEnter {
|
||||
@ -256,4 +318,14 @@ internal class DefaultFileService @Inject constructor(
|
||||
override fun clearCache() {
|
||||
downloadFolder.deleteRecursively()
|
||||
}
|
||||
|
||||
override fun clearDecryptedCache() {
|
||||
decryptedFolder.deleteRecursively()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ENCRYPTED_FILENAME = "encrypted.bin"
|
||||
// The extension would be added from the mimetype
|
||||
private const val DEFAULT_FILENAME = "file"
|
||||
}
|
||||
}
|
||||
|
@ -50,6 +50,7 @@ import org.matrix.android.sdk.internal.database.EventInsertLiveObserver
|
||||
import org.matrix.android.sdk.internal.database.RealmSessionProvider
|
||||
import org.matrix.android.sdk.internal.database.SessionRealmConfigurationFactory
|
||||
import org.matrix.android.sdk.internal.di.Authenticated
|
||||
import org.matrix.android.sdk.internal.di.CacheDirectory
|
||||
import org.matrix.android.sdk.internal.di.DeviceId
|
||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.di.SessionDownloadsDirectory
|
||||
@ -169,9 +170,9 @@ internal abstract class SessionModule {
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@SessionDownloadsDirectory
|
||||
fun providesCacheDir(@SessionId sessionId: String,
|
||||
context: Context): File {
|
||||
return File(context.cacheDir, "downloads/$sessionId")
|
||||
fun providesDownloadsCacheDir(@SessionId sessionId: String,
|
||||
@CacheDirectory cacheFile: File): File {
|
||||
return File(cacheFile, "downloads/$sessionId")
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
|
@ -20,6 +20,9 @@ import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ContentUploadResponse(
|
||||
internal data class ContentUploadResponse(
|
||||
/**
|
||||
* Required. The MXC URI to the uploaded content.
|
||||
*/
|
||||
@Json(name = "content_uri") val contentUri: String
|
||||
)
|
||||
|
@ -174,14 +174,15 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
||||
}
|
||||
}
|
||||
|
||||
val encryptedFile: File?
|
||||
val contentUploadResponse = if (params.isEncrypted) {
|
||||
Timber.v("## FileService: Encrypt file")
|
||||
|
||||
val tmpEncrypted = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir)
|
||||
encryptedFile = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir)
|
||||
.also { filesToDelete.add(it) }
|
||||
|
||||
uploadedFileEncryptedFileInfo =
|
||||
MXEncryptedAttachments.encrypt(fileToUpload.inputStream(), attachment.getSafeMimeType(), tmpEncrypted) { read, total ->
|
||||
MXEncryptedAttachments.encrypt(fileToUpload.inputStream(), attachment.getSafeMimeType(), encryptedFile) { read, total ->
|
||||
notifyTracker(params) {
|
||||
contentUploadStateTracker.setEncrypting(it, read.toLong(), total.toLong())
|
||||
}
|
||||
@ -190,18 +191,23 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
||||
Timber.v("## FileService: Uploading file")
|
||||
|
||||
fileUploader
|
||||
.uploadFile(tmpEncrypted, attachment.name, "application/octet-stream", progressListener)
|
||||
.uploadFile(encryptedFile, attachment.name, "application/octet-stream", progressListener)
|
||||
} else {
|
||||
Timber.v("## FileService: Clear file")
|
||||
encryptedFile = null
|
||||
fileUploader
|
||||
.uploadFile(fileToUpload, attachment.name, attachment.getSafeMimeType(), progressListener)
|
||||
}
|
||||
|
||||
Timber.v("## FileService: Update cache storage for ${contentUploadResponse.contentUri}")
|
||||
try {
|
||||
context.contentResolver.openInputStream(attachment.queryUri)?.let {
|
||||
fileService.storeDataFor(contentUploadResponse.contentUri, params.attachment.getSafeMimeType(), it)
|
||||
}
|
||||
fileService.storeDataFor(
|
||||
mxcUrl = contentUploadResponse.contentUri,
|
||||
filename = params.attachment.name,
|
||||
mimeType = params.attachment.getSafeMimeType(),
|
||||
originalFile = workingFile,
|
||||
encryptedFile = encryptedFile
|
||||
)
|
||||
Timber.v("## FileService: cache storage updated")
|
||||
} catch (failure: Throwable) {
|
||||
Timber.e(failure, "## FileService: Failed to update file cache")
|
||||
|
@ -177,7 +177,7 @@ internal class DefaultSendService @AssistedInject constructor(
|
||||
val attachmentData = ContentAttachmentData(
|
||||
size = messageContent.info!!.size,
|
||||
mimeType = messageContent.info.mimeType!!,
|
||||
name = messageContent.body,
|
||||
name = messageContent.getFileName(),
|
||||
queryUri = Uri.parse(messageContent.url),
|
||||
type = ContentAttachmentData.Type.FILE
|
||||
)
|
||||
|
@ -25,7 +25,6 @@ import org.matrix.android.sdk.api.failure.isTokenError
|
||||
import org.matrix.android.sdk.api.session.sync.SyncState
|
||||
import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker
|
||||
import org.matrix.android.sdk.internal.session.sync.SyncTask
|
||||
import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker
|
||||
import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver
|
||||
import org.matrix.android.sdk.internal.util.Debouncer
|
||||
import org.matrix.android.sdk.internal.util.createUIHandler
|
||||
@ -50,14 +49,13 @@ private const val RETRY_WAIT_TIME_MS = 10_000L
|
||||
private const val DEFAULT_LONG_POOL_TIMEOUT = 30_000L
|
||||
|
||||
internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
|
||||
private val typingUsersTracker: DefaultTypingUsersTracker,
|
||||
private val networkConnectivityChecker: NetworkConnectivityChecker,
|
||||
private val backgroundDetectionObserver: BackgroundDetectionObserver,
|
||||
private val activeCallHandler: ActiveCallHandler
|
||||
) : Thread("SyncThread"), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener {
|
||||
|
||||
private var state: SyncState = SyncState.Idle
|
||||
private var liveState = MutableLiveData<SyncState>(state)
|
||||
private var liveState = MutableLiveData(state)
|
||||
private val lock = Object()
|
||||
private val syncScope = CoroutineScope(SupervisorJob())
|
||||
private val debouncer = Debouncer(createUIHandler())
|
||||
@ -231,7 +229,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
|
||||
return
|
||||
}
|
||||
state = newState
|
||||
debouncer.debounce("post_state", Runnable {
|
||||
debouncer.debounce("post_state", {
|
||||
liveState.value = newState
|
||||
}, 150)
|
||||
}
|
||||
|
@ -25,6 +25,9 @@ import java.io.InputStream
|
||||
*/
|
||||
@WorkerThread
|
||||
fun writeToFile(inputStream: InputStream, outputFile: File) {
|
||||
// Ensure the parent folder exists, else it will crash
|
||||
outputFile.parentFile?.mkdirs()
|
||||
|
||||
outputFile.outputStream().use {
|
||||
inputStream.copyTo(it)
|
||||
}
|
||||
|
@ -28,7 +28,6 @@ import im.vector.app.core.di.ActiveSessionHolder
|
||||
import im.vector.app.features.media.ImageContentRenderer
|
||||
import okhttp3.OkHttpClient
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.session.file.FileService
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
@ -110,11 +109,9 @@ class VectorGlideDataFetcher(private val activeSessionHolder: ActiveSessionHolde
|
||||
}
|
||||
// Use the file vector service, will avoid flickering and redownload after upload
|
||||
fileService.downloadFile(
|
||||
downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE,
|
||||
mimeType = data.mimeType,
|
||||
id = data.eventId,
|
||||
url = data.url,
|
||||
fileName = data.filename,
|
||||
mimeType = data.mimeType,
|
||||
url = data.url,
|
||||
elementToDecrypt = data.elementToDecrypt,
|
||||
callback = object : MatrixCallback<File> {
|
||||
override fun onSuccess(data: File) {
|
||||
|
@ -71,12 +71,18 @@ class HomeActivityViewModel @AssistedInject constructor(
|
||||
private var onceTrusted = false
|
||||
|
||||
init {
|
||||
cleanupFiles()
|
||||
observeInitialSync()
|
||||
mayBeInitializeCrossSigning()
|
||||
checkSessionPushIsOn()
|
||||
observeCrossSigningReset()
|
||||
}
|
||||
|
||||
private fun cleanupFiles() {
|
||||
// Mitigation: delete all cached decrypted files each time the application is started.
|
||||
activeSessionHolder.getSafeActiveSession()?.fileService()?.clearDecryptedCache()
|
||||
}
|
||||
|
||||
private fun observeCrossSigningReset() {
|
||||
val safeActiveSession = activeSessionHolder.getSafeActiveSession() ?: return
|
||||
|
||||
|
@ -174,7 +174,6 @@ import org.matrix.android.sdk.api.session.Session
|
||||
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.toModel
|
||||
import org.matrix.android.sdk.api.session.file.FileService
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||
@ -185,7 +184,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
|
||||
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 org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
@ -194,7 +192,6 @@ import org.matrix.android.sdk.api.session.widgets.model.Widget
|
||||
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
|
||||
import timber.log.Timber
|
||||
@ -1667,12 +1664,7 @@ class RoomDetailFragment @Inject constructor(
|
||||
shareText(requireContext(), action.messageContent.body)
|
||||
} else if (action.messageContent is MessageWithAttachmentContent) {
|
||||
session.fileService().downloadFile(
|
||||
downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
|
||||
id = action.eventId,
|
||||
fileName = action.messageContent.body,
|
||||
mimeType = action.messageContent.mimeType,
|
||||
url = action.messageContent.getFileUrl(),
|
||||
elementToDecrypt = action.messageContent.encryptedFileInfo?.toElementToDecrypt(),
|
||||
messageContent = action.messageContent,
|
||||
callback = object : MatrixCallback<File> {
|
||||
override fun onSuccess(data: File) {
|
||||
if (isAdded) {
|
||||
@ -1702,12 +1694,7 @@ class RoomDetailFragment @Inject constructor(
|
||||
return
|
||||
}
|
||||
session.fileService().downloadFile(
|
||||
downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
|
||||
id = action.eventId,
|
||||
fileName = action.messageContent.body,
|
||||
mimeType = action.messageContent.mimeType,
|
||||
url = action.messageContent.getFileUrl(),
|
||||
elementToDecrypt = action.messageContent.encryptedFileInfo?.toElementToDecrypt(),
|
||||
messageContent = action.messageContent,
|
||||
callback = object : MatrixCallback<File> {
|
||||
override fun onSuccess(data: File) {
|
||||
if (isAdded) {
|
||||
|
@ -69,7 +69,6 @@ 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.toContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.file.FileService
|
||||
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
|
||||
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
|
||||
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
|
||||
@ -80,7 +79,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
||||
import org.matrix.android.sdk.api.session.room.model.message.OptionItem
|
||||
import org.matrix.android.sdk.api.session.room.model.message.getFileName
|
||||
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
|
||||
import org.matrix.android.sdk.api.session.room.model.tombstone.RoomTombstoneContent
|
||||
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
|
||||
@ -92,7 +90,6 @@ import org.matrix.android.sdk.api.session.room.timeline.getTextEditableContent
|
||||
import org.matrix.android.sdk.api.session.widgets.model.Widget
|
||||
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
|
||||
import org.matrix.android.sdk.api.util.toOptional
|
||||
import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
|
||||
import org.matrix.android.sdk.internal.util.awaitCallback
|
||||
@ -1010,10 +1007,10 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
}
|
||||
|
||||
private fun handleOpenOrDownloadFile(action: RoomDetailAction.DownloadOrOpen) {
|
||||
val mxcUrl = action.messageFileContent.getFileUrl()
|
||||
val mxcUrl = action.messageFileContent.getFileUrl() ?: return
|
||||
val isLocalSendingFile = action.senderId == session.myUserId
|
||||
&& mxcUrl?.startsWith("content://") ?: false
|
||||
val isDownloaded = mxcUrl?.let { session.fileService().isFileInCache(it, action.messageFileContent.mimeType) } ?: false
|
||||
&& mxcUrl.startsWith("content://")
|
||||
val isDownloaded = session.fileService().isFileInCache(action.messageFileContent)
|
||||
if (isLocalSendingFile) {
|
||||
tryOrNull { Uri.parse(mxcUrl) }?.let {
|
||||
_viewEvents.post(RoomDetailViewEvents.OpenFile(
|
||||
@ -1024,7 +1021,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
}
|
||||
} else if (isDownloaded) {
|
||||
// we can open it
|
||||
session.fileService().getTemporarySharableURI(mxcUrl!!, action.messageFileContent.mimeType)?.let { uri ->
|
||||
session.fileService().getTemporarySharableURI(action.messageFileContent)?.let { uri ->
|
||||
_viewEvents.post(RoomDetailViewEvents.OpenFile(
|
||||
action.messageFileContent.mimeType,
|
||||
uri,
|
||||
@ -1033,12 +1030,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
}
|
||||
} else {
|
||||
session.fileService().downloadFile(
|
||||
downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE,
|
||||
id = action.eventId,
|
||||
fileName = action.messageFileContent.getFileName(),
|
||||
mimeType = action.messageFileContent.mimeType,
|
||||
url = mxcUrl,
|
||||
elementToDecrypt = action.messageFileContent.encryptedFileInfo?.toElementToDecrypt(),
|
||||
messageContent = action.messageFileContent,
|
||||
callback = object : MatrixCallback<File> {
|
||||
override fun onSuccess(data: File) {
|
||||
_viewEvents.post(RoomDetailViewEvents.DownloadFileState(
|
||||
|
@ -82,10 +82,9 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
|
||||
when (cryptoError) {
|
||||
MXCryptoError.ErrorType.KEYS_WITHHELD -> {
|
||||
span {
|
||||
apply {
|
||||
drawableProvider.getDrawable(R.drawable.ic_forbidden, colorFromAttribute)?.let {
|
||||
image(it, "baseline")
|
||||
}
|
||||
drawableProvider.getDrawable(R.drawable.ic_forbidden, colorFromAttribute)?.let {
|
||||
image(it, "baseline")
|
||||
+" "
|
||||
}
|
||||
span(stringProvider.getString(R.string.notice_crypto_unable_to_decrypt_final)) {
|
||||
textStyle = "italic"
|
||||
@ -95,10 +94,9 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
|
||||
}
|
||||
else -> {
|
||||
span {
|
||||
apply {
|
||||
drawableProvider.getDrawable(R.drawable.ic_clock, colorFromAttribute)?.let {
|
||||
image(it, "baseline")
|
||||
}
|
||||
drawableProvider.getDrawable(R.drawable.ic_clock, colorFromAttribute)?.let {
|
||||
image(it, "baseline")
|
||||
+" "
|
||||
}
|
||||
span(stringProvider.getString(R.string.notice_crypto_unable_to_decrypt_friendly)) {
|
||||
textStyle = "italic"
|
||||
|
@ -84,6 +84,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageVerification
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.OPTION_TYPE_BUTTONS
|
||||
import org.matrix.android.sdk.api.session.room.model.message.OPTION_TYPE_POLL
|
||||
import org.matrix.android.sdk.api.session.room.model.message.getFileName
|
||||
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
|
||||
@ -204,7 +205,12 @@ class MessageItemFactory @Inject constructor(
|
||||
return MessageFileItem_()
|
||||
.attributes(attributes)
|
||||
.izLocalFile(fileUrl.isLocalFile())
|
||||
.izDownloaded(session.fileService().isFileInCache(fileUrl, messageContent.mimeType))
|
||||
.izDownloaded(session.fileService().isFileInCache(
|
||||
fileUrl,
|
||||
messageContent.getFileName(),
|
||||
messageContent.mimeType,
|
||||
messageContent.encryptedFileInfo?.toElementToDecrypt())
|
||||
)
|
||||
.mxcUrl(fileUrl)
|
||||
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
|
||||
.contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder)
|
||||
@ -264,7 +270,7 @@ class MessageItemFactory @Inject constructor(
|
||||
.attributes(attributes)
|
||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||
.izLocalFile(messageContent.getFileUrl().isLocalFile())
|
||||
.izDownloaded(session.fileService().isFileInCache(mxcUrl, messageContent.mimeType))
|
||||
.izDownloaded(session.fileService().isFileInCache(messageContent))
|
||||
.mxcUrl(mxcUrl)
|
||||
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
|
||||
.contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder)
|
||||
|
@ -153,12 +153,10 @@ abstract class BaseAttachmentProvider<Type>(
|
||||
} else {
|
||||
target.onVideoFileLoading(info.uid)
|
||||
fileService.downloadFile(
|
||||
downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE,
|
||||
id = data.eventId,
|
||||
mimeType = data.mimeType,
|
||||
elementToDecrypt = data.elementToDecrypt,
|
||||
fileName = data.filename,
|
||||
mimeType = data.mimeType,
|
||||
url = data.url,
|
||||
elementToDecrypt = data.elementToDecrypt,
|
||||
callback = object : MatrixCallback<File> {
|
||||
override fun onSuccess(data: File) {
|
||||
target.onVideoFileReady(info.uid, data)
|
||||
|
@ -77,11 +77,9 @@ class DataAttachmentRoomProvider(
|
||||
override fun getFileForSharing(position: Int, callback: (File?) -> Unit) {
|
||||
val item = getItem(position)
|
||||
fileService.downloadFile(
|
||||
downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
|
||||
id = item.eventId,
|
||||
fileName = item.filename,
|
||||
mimeType = item.mimeType,
|
||||
url = item.url ?: "",
|
||||
url = item.url,
|
||||
elementToDecrypt = item.elementToDecrypt,
|
||||
callback = object : MatrixCallback<File> {
|
||||
override fun onSuccess(data: File) {
|
||||
|
@ -23,6 +23,7 @@ import android.view.View
|
||||
import android.widget.ImageView
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import com.bumptech.glide.load.DataSource
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.load.engine.GlideException
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||
import com.bumptech.glide.request.RequestListener
|
||||
@ -129,6 +130,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
|
||||
GlideApp
|
||||
.with(contextView)
|
||||
.load(data)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
} else {
|
||||
// Clear image
|
||||
val resolvedUrl = resolveUrl(data)
|
||||
@ -183,6 +185,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
|
||||
GlideApp
|
||||
.with(imageView)
|
||||
.load(data)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
} else {
|
||||
// Clear image
|
||||
val resolvedUrl = resolveUrl(data)
|
||||
@ -214,14 +217,16 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
|
||||
.into(imageView)
|
||||
}
|
||||
|
||||
fun createGlideRequest(data: Data, mode: Mode, imageView: ImageView, size: Size): GlideRequest<Drawable> {
|
||||
private fun createGlideRequest(data: Data, mode: Mode, imageView: ImageView, size: Size): GlideRequest<Drawable> {
|
||||
return createGlideRequest(data, mode, GlideApp.with(imageView), size)
|
||||
}
|
||||
|
||||
fun createGlideRequest(data: Data, mode: Mode, glideRequests: GlideRequests, size: Size = processSize(data, mode)): GlideRequest<Drawable> {
|
||||
return if (data.elementToDecrypt != null) {
|
||||
// Encrypted image
|
||||
glideRequests.load(data)
|
||||
glideRequests
|
||||
.load(data)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
} else {
|
||||
// Clear image
|
||||
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
|
||||
|
@ -125,8 +125,6 @@ class RoomEventsAttachmentProvider(
|
||||
as? MessageWithAttachmentContent
|
||||
?: return@let
|
||||
fileService.downloadFile(
|
||||
downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
|
||||
id = timelineEvent.eventId,
|
||||
fileName = messageContent.body,
|
||||
mimeType = messageContent.mimeType,
|
||||
url = messageContent.getFileUrl(),
|
||||
|
@ -27,7 +27,6 @@ import im.vector.app.core.error.ErrorFormatter
|
||||
import im.vector.app.core.utils.isLocalFile
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.session.file.FileService
|
||||
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
@ -76,8 +75,6 @@ class VideoContentRenderer @Inject constructor(private val activeSessionHolder:
|
||||
|
||||
activeSessionHolder.getActiveSession().fileService()
|
||||
.downloadFile(
|
||||
downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE,
|
||||
id = data.eventId,
|
||||
fileName = data.filename,
|
||||
mimeType = data.mimeType,
|
||||
url = data.url,
|
||||
@ -116,8 +113,6 @@ class VideoContentRenderer @Inject constructor(private val activeSessionHolder:
|
||||
|
||||
activeSessionHolder.getActiveSession().fileService()
|
||||
.downloadFile(
|
||||
downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE,
|
||||
id = data.eventId,
|
||||
fileName = data.filename,
|
||||
mimeType = data.mimeType,
|
||||
url = data.url,
|
||||
|
@ -30,10 +30,7 @@ import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.file.FileService
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
||||
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
|
||||
import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt
|
||||
import org.matrix.android.sdk.internal.util.awaitCallback
|
||||
import org.matrix.android.sdk.rx.rx
|
||||
import org.matrix.android.sdk.rx.unwrap
|
||||
@ -134,12 +131,7 @@ class RoomUploadsViewModel @AssistedInject constructor(
|
||||
try {
|
||||
val file = awaitCallback<File> {
|
||||
session.fileService().downloadFile(
|
||||
downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
|
||||
id = action.uploadEvent.eventId,
|
||||
fileName = action.uploadEvent.contentWithAttachmentContent.body,
|
||||
url = action.uploadEvent.contentWithAttachmentContent.getFileUrl(),
|
||||
mimeType = action.uploadEvent.contentWithAttachmentContent.mimeType,
|
||||
elementToDecrypt = action.uploadEvent.contentWithAttachmentContent.encryptedFileInfo?.toElementToDecrypt(),
|
||||
messageContent = action.uploadEvent.contentWithAttachmentContent,
|
||||
callback = it
|
||||
)
|
||||
}
|
||||
@ -155,12 +147,7 @@ class RoomUploadsViewModel @AssistedInject constructor(
|
||||
try {
|
||||
val file = awaitCallback<File> {
|
||||
session.fileService().downloadFile(
|
||||
downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
|
||||
id = action.uploadEvent.eventId,
|
||||
fileName = action.uploadEvent.contentWithAttachmentContent.body,
|
||||
mimeType = action.uploadEvent.contentWithAttachmentContent.mimeType,
|
||||
url = action.uploadEvent.contentWithAttachmentContent.getFileUrl(),
|
||||
elementToDecrypt = action.uploadEvent.contentWithAttachmentContent.encryptedFileInfo?.toElementToDecrypt(),
|
||||
messageContent = action.uploadEvent.contentWithAttachmentContent,
|
||||
callback = it)
|
||||
}
|
||||
_viewEvents.post(RoomUploadsViewEvents.FileReadyForSaving(file, action.uploadEvent.contentWithAttachmentContent.body))
|
||||
|
Loading…
Reference in New Issue
Block a user