diff --git a/matrix-sdk-android/src/main/AndroidManifest.xml b/matrix-sdk-android/src/main/AndroidManifest.xml index e8762b21f2..94b2db2bf1 100644 --- a/matrix-sdk-android/src/main/AndroidManifest.xml +++ b/matrix-sdk-android/src/main/AndroidManifest.xml @@ -13,8 +13,20 @@ android:authorities="${applicationId}.workmanager-init" android:exported="false" tools:node="remove" /> - + + + + - diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt index 018aae4580..b5418b66ca 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt @@ -46,6 +46,7 @@ import im.vector.matrix.android.api.session.terms.TermsService import im.vector.matrix.android.api.session.typing.TypingUsersTracker import im.vector.matrix.android.api.session.user.UserService import im.vector.matrix.android.api.session.widgets.WidgetService +import im.vector.matrix.android.internal.session.download.ContentDownloadStateTracker /** * This interface defines interactions with a session. @@ -152,6 +153,11 @@ interface Session : */ fun typingUsersTracker(): TypingUsersTracker + /** + * Returns the ContentDownloadStateTracker associated with the session + */ + fun contentDownloadProgressTracker(): ContentDownloadStateTracker + /** * Returns the cryptoService associated with the session */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt index 1e178484e9..975e72e088 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt @@ -235,3 +235,11 @@ fun Event.isVideoMessage(): Boolean { else -> false } } + +fun Event.isFileMessage(): Boolean { + return getClearType() == EventType.MESSAGE + && when (getClearContent()?.toModel()?.msgType) { + MessageType.MSGTYPE_FILE -> true + else -> false + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/file/FileService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/file/FileService.kt index 32fb1a6ab0..d566de475a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/file/FileService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/file/FileService.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.api.session.file +import android.net.Uri import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt @@ -50,7 +51,16 @@ interface FileService { downloadMode: DownloadMode, id: String, fileName: String, + mimeType: String?, url: String?, elementToDecrypt: ElementToDecrypt?, callback: MatrixCallback): Cancelable + + fun isFileInCache(mxcUrl: String, mimeType: String?): Boolean + + /** + * 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? } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/file/MatrixSDKFileProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/file/MatrixSDKFileProvider.kt new file mode 100644 index 0000000000..31d85eefb0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/file/MatrixSDKFileProvider.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.file + +import android.net.Uri +import androidx.core.content.FileProvider + +/** + * We have to declare our own file provider to avoid collision with apps using the sdk + * and having their own + */ +class MatrixSDKFileProvider : FileProvider() { + override fun getType(uri: Uri): String? { + return super.getType(uri) ?: "plain/text" + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageAudioContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageAudioContent.kt index 248e782a74..a0f4655f4b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageAudioContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageAudioContent.kt @@ -51,4 +51,8 @@ data class MessageAudioContent( * Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption. */ @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null -) : MessageWithAttachmentContent +) : MessageWithAttachmentContent { + + override val mimeType: String? + get() = encryptedFileInfo?.mimetype ?: audioInfo?.mimeType +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageFileContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageFileContent.kt index f770a2ccea..067d08e5b8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageFileContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageFileContent.kt @@ -16,7 +16,7 @@ package im.vector.matrix.android.api.session.room.model.message -import android.content.ClipDescription +import android.webkit.MimeTypeMap import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import im.vector.matrix.android.api.session.events.model.Content @@ -59,12 +59,12 @@ data class MessageFileContent( @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null ) : MessageWithAttachmentContent { - fun getMimeType(): String { - // Mimetype default to plain text, should not be used - return encryptedFileInfo?.mimetype + override val mimeType: String? + get() = encryptedFileInfo?.mimetype ?: info?.mimeType - ?: ClipDescription.MIMETYPE_TEXT_PLAIN - } + ?: MimeTypeMap.getFileExtensionFromUrl(filename ?: body)?.let { extension -> + MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) + } fun getFileName(): String { return filename ?: body diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageImageContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageImageContent.kt index f50a108947..75ae5f0323 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageImageContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageImageContent.kt @@ -52,4 +52,7 @@ data class MessageImageContent( * Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption. */ @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null -) : MessageImageInfoContent +) : MessageImageInfoContent { + override val mimeType: String? + get() = encryptedFileInfo?.mimetype ?: info?.mimeType ?: "image/*" +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageStickerContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageStickerContent.kt index 9198537bff..6730768d7d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageStickerContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageStickerContent.kt @@ -52,4 +52,7 @@ data class MessageStickerContent( * Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption. */ @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null -) : MessageImageInfoContent +) : MessageImageInfoContent { + override val mimeType: String? + get() = encryptedFileInfo?.mimetype ?: info?.mimeType +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVideoContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVideoContent.kt index 88d2d72d15..34d599595f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVideoContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVideoContent.kt @@ -51,4 +51,7 @@ data class MessageVideoContent( * Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption. */ @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null -) : MessageWithAttachmentContent +) : MessageWithAttachmentContent { + override val mimeType: String? + get() = encryptedFileInfo?.mimetype ?: videoInfo?.mimeType +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageWithAttachmentContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageWithAttachmentContent.kt index 9caf38013f..0613f69c56 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageWithAttachmentContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageWithAttachmentContent.kt @@ -31,9 +31,13 @@ interface MessageWithAttachmentContent : MessageContent { * Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption. */ val encryptedFileInfo: EncryptedFileInfo? + + val mimeType: String? } /** * Get the url of the encrypted file or of the file */ fun MessageWithAttachmentContent.getFileUrl() = encryptedFileInfo?.url ?: url + +fun MessageWithAttachmentContent.getFileName() = (this as? MessageFileContent)?.getFileName() ?: body diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/AuthQualifiers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/AuthQualifiers.kt index 105d904329..98af702184 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/AuthQualifiers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/AuthQualifiers.kt @@ -33,3 +33,7 @@ internal annotation class Unauthenticated @Qualifier @Retention(AnnotationRetention.RUNTIME) internal annotation class UnauthenticatedWithCertificate + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class WithProgress diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/FileQualifiers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/FileQualifiers.kt index 5dfc04539a..5a7ac1bb24 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/FileQualifiers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/FileQualifiers.kt @@ -24,7 +24,7 @@ internal annotation class SessionFilesDirectory @Qualifier @Retention(AnnotationRetention.RUNTIME) -internal annotation class SessionCacheDirectory +internal annotation class SessionDownloadsDirectory @Qualifier @Retention(AnnotationRetention.RUNTIME) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt index 222c0506f5..2d6e955cbb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt @@ -16,6 +16,10 @@ package im.vector.matrix.android.internal.session +import android.content.Context +import android.net.Uri +import android.webkit.MimeTypeMap +import androidx.core.content.FileProvider import arrow.core.Try import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.content.ContentUrlResolver @@ -25,8 +29,8 @@ import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments import im.vector.matrix.android.internal.di.CacheDirectory import im.vector.matrix.android.internal.di.ExternalFilesDirectory -import im.vector.matrix.android.internal.di.SessionCacheDirectory -import im.vector.matrix.android.internal.di.UnauthenticatedWithCertificate +import im.vector.matrix.android.internal.di.SessionDownloadsDirectory +import im.vector.matrix.android.internal.di.WithProgress import im.vector.matrix.android.internal.extensions.foldToCallback import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers @@ -39,22 +43,28 @@ import okhttp3.Request import timber.log.Timber import java.io.File import java.io.IOException +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?, - @SessionCacheDirectory + @SessionDownloadsDirectory private val sessionCacheDirectory: File, private val contentUrlResolver: ContentUrlResolver, - @UnauthenticatedWithCertificate + @WithProgress private val okHttpClient: OkHttpClient, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val taskExecutor: TaskExecutor ) : FileService { + private fun String.safeFileName() = URLEncoder.encode(this, Charsets.US_ASCII.displayName()) + + private val downloadFolder = File(sessionCacheDirectory, "MF") + /** * Download file in the cache folder, and eventually decrypt it * TODO implement clear file, to delete "MF" @@ -63,23 +73,28 @@ internal class DefaultFileService @Inject constructor( override fun downloadFile(downloadMode: FileService.DownloadMode, id: String, fileName: String, + mimeType: String?, url: String?, elementToDecrypt: ElementToDecrypt?, callback: MatrixCallback): Cancelable { return taskExecutor.executorScope.launch(coroutineDispatchers.main) { withContext(coroutineDispatchers.io) { Try { - val folder = File(sessionCacheDirectory, "MF") - if (!folder.exists()) { - folder.mkdirs() + val unwrappedUrl = url ?: throw IllegalArgumentException("url is null") + if (!downloadFolder.exists()) { + downloadFolder.mkdirs() } - File(folder, fileName) + // 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()) { val resolvedUrl = contentUrlResolver.resolveFullSize(url) ?: return@flatMap Try.Failure(IllegalArgumentException("url is null")) val request = Request.Builder() .url(resolvedUrl) + .header("matrix-sdk:mxc_URL", url ?: "") .build() val response = try { @@ -121,6 +136,27 @@ internal class DefaultFileService @Inject constructor( }.toCancelable() } + 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, mimeType: String?): Boolean { + return File(downloadFolder, fileForUrl(mxcUrl, mimeType)).exists() + } + + /** + * 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? { + // this string could be extracted no? + val authority = "${context.packageName}.mx-sdk.fileprovider" + val targetFile = File(downloadFolder, fileForUrl(mxcUrl, mimeType)) + if (!targetFile.exists()) return null + return FileProvider.getUriForFile(context, authority, targetFile) + } + private fun copyFile(file: File, downloadMode: FileService.DownloadMode): File { return when (downloadMode) { FileService.DownloadMode.TO_EXPORT -> diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt index e32ba7e63c..02c9921de7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt @@ -53,6 +53,7 @@ import im.vector.matrix.android.internal.auth.SessionParamsStore import im.vector.matrix.android.internal.crypto.DefaultCryptoService import im.vector.matrix.android.internal.di.SessionId import im.vector.matrix.android.internal.di.WorkManagerProvider +import im.vector.matrix.android.internal.session.download.ContentDownloadStateTracker import im.vector.matrix.android.internal.session.identity.DefaultIdentityService import im.vector.matrix.android.internal.session.room.timeline.TimelineEventDecryptor import im.vector.matrix.android.internal.session.sync.SyncTokenStore @@ -100,6 +101,7 @@ internal class DefaultSession @Inject constructor( private val sessionParamsStore: SessionParamsStore, private val contentUploadProgressTracker: ContentUploadStateTracker, private val typingUsersTracker: TypingUsersTracker, + private val contentDownloadStateTracker: ContentDownloadStateTracker, private val initialSyncProgressService: Lazy, private val homeServerCapabilitiesService: Lazy, private val accountDataService: Lazy, @@ -239,6 +241,8 @@ internal class DefaultSession @Inject constructor( override fun typingUsersTracker() = typingUsersTracker + override fun contentDownloadProgressTracker(): ContentDownloadStateTracker = contentDownloadStateTracker + override fun cryptoService(): CryptoService = cryptoService.get() override fun identityService() = defaultIdentityService diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index f084bec924..1f974b0e2c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -43,14 +43,15 @@ import im.vector.matrix.android.internal.crypto.verification.VerificationMessage import im.vector.matrix.android.internal.database.SessionRealmConfigurationFactory import im.vector.matrix.android.internal.di.Authenticated import im.vector.matrix.android.internal.di.DeviceId -import im.vector.matrix.android.internal.di.SessionCacheDirectory import im.vector.matrix.android.internal.di.SessionDatabase +import im.vector.matrix.android.internal.di.SessionDownloadsDirectory import im.vector.matrix.android.internal.di.SessionFilesDirectory import im.vector.matrix.android.internal.di.SessionId import im.vector.matrix.android.internal.di.Unauthenticated import im.vector.matrix.android.internal.di.UnauthenticatedWithCertificate import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.di.UserMd5 +import im.vector.matrix.android.internal.di.WithProgress import im.vector.matrix.android.internal.eventbus.EventBusTimberLogger import im.vector.matrix.android.internal.network.DefaultNetworkConnectivityChecker import im.vector.matrix.android.internal.network.FallbackNetworkCallbackStrategy @@ -60,9 +61,11 @@ import im.vector.matrix.android.internal.network.PreferredNetworkCallbackStrateg import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.network.httpclient.addAccessTokenInterceptor import im.vector.matrix.android.internal.network.httpclient.addSocketFactory +import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor import im.vector.matrix.android.internal.network.token.AccessTokenProvider import im.vector.matrix.android.internal.network.token.HomeserverAccessTokenProvider import im.vector.matrix.android.internal.session.call.CallEventObserver +import im.vector.matrix.android.internal.session.download.DownloadProgressInterceptor import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater import im.vector.matrix.android.internal.session.homeserver.DefaultHomeServerCapabilitiesService import im.vector.matrix.android.internal.session.identity.DefaultIdentityService @@ -160,10 +163,10 @@ internal abstract class SessionModule { @JvmStatic @Provides - @SessionCacheDirectory + @SessionDownloadsDirectory fun providesCacheDir(@SessionId sessionId: String, context: Context): File { - return File(context.cacheDir, sessionId) + return File(context.cacheDir, "downloads/$sessionId") } @JvmStatic @@ -216,6 +219,27 @@ internal abstract class SessionModule { .build() } + @JvmStatic + @Provides + @SessionScope + @WithProgress + fun providesProgressOkHttpClient(@UnauthenticatedWithCertificate okHttpClient: OkHttpClient, + downloadProgressInterceptor: DownloadProgressInterceptor): OkHttpClient { + return okHttpClient.newBuilder() + .apply { + // Remove the previous CurlLoggingInterceptor, to add it after the accessTokenInterceptor + val existingCurlInterceptors = interceptors().filterIsInstance() + interceptors().removeAll(existingCurlInterceptors) + + addInterceptor(downloadProgressInterceptor) + + // Re add eventually the curl logging interceptors + existingCurlInterceptors.forEach { + addInterceptor(it) + } + }.build() + } + @JvmStatic @Provides @SessionScope diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/cleanup/CleanupSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/cleanup/CleanupSession.kt index ebd0fad39c..56c7eb557b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/cleanup/CleanupSession.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/cleanup/CleanupSession.kt @@ -22,7 +22,7 @@ import im.vector.matrix.android.internal.auth.SessionParamsStore import im.vector.matrix.android.internal.crypto.CryptoModule import im.vector.matrix.android.internal.database.RealmKeysUtils import im.vector.matrix.android.internal.di.CryptoDatabase -import im.vector.matrix.android.internal.di.SessionCacheDirectory +import im.vector.matrix.android.internal.di.SessionDownloadsDirectory import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.di.SessionFilesDirectory import im.vector.matrix.android.internal.di.SessionId @@ -44,7 +44,7 @@ internal class CleanupSession @Inject constructor( @SessionDatabase private val clearSessionDataTask: ClearCacheTask, @CryptoDatabase private val clearCryptoDataTask: ClearCacheTask, @SessionFilesDirectory private val sessionFiles: File, - @SessionCacheDirectory private val sessionCache: File, + @SessionDownloadsDirectory private val sessionCache: File, private val realmKeysUtils: RealmKeysUtils, @SessionDatabase private val realmSessionConfiguration: RealmConfiguration, @CryptoDatabase private val realmCryptoConfiguration: RealmConfiguration, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ContentModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ContentModule.kt index 577626c8ac..eaddddb223 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ContentModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ContentModule.kt @@ -20,6 +20,8 @@ import dagger.Binds import dagger.Module import im.vector.matrix.android.api.session.content.ContentUploadStateTracker import im.vector.matrix.android.api.session.content.ContentUrlResolver +import im.vector.matrix.android.internal.session.download.ContentDownloadStateTracker +import im.vector.matrix.android.internal.session.download.DefaultContentDownloadStateTracker @Module internal abstract class ContentModule { @@ -27,6 +29,9 @@ internal abstract class ContentModule { @Binds abstract fun bindContentUploadStateTracker(tracker: DefaultContentUploadStateTracker): ContentUploadStateTracker + @Binds + abstract fun bindContentDownloadStateTracker(tracker: DefaultContentDownloadStateTracker): ContentDownloadStateTracker + @Binds abstract fun bindContentUrlResolver(resolver: DefaultContentUrlResolver): ContentUrlResolver } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/download/ContentDownloadStateTracker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/download/ContentDownloadStateTracker.kt new file mode 100644 index 0000000000..d40941d8df --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/download/ContentDownloadStateTracker.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.download + +interface ContentDownloadStateTracker { + fun track(key: String, updateListener: UpdateListener) + fun unTrack(key: String, updateListener: UpdateListener) + fun clear() + + sealed class State { + object Idle : State() + data class Downloading(val current: Long, val total: Long, val indeterminate: Boolean) : State() + object Decrypting : State() + object Success : State() + data class Failure(val errorCode: Int) : State() + } + + interface UpdateListener { + fun onDownloadStateUpdate(state: State) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/download/DefaultContentDownloadStateTracker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/download/DefaultContentDownloadStateTracker.kt new file mode 100644 index 0000000000..3ff9a2ba05 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/download/DefaultContentDownloadStateTracker.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.download + +import android.os.Handler +import android.os.Looper +import im.vector.matrix.android.api.extensions.tryThis +import im.vector.matrix.android.internal.session.SessionScope +import timber.log.Timber +import javax.inject.Inject + +@SessionScope +class DefaultContentDownloadStateTracker @Inject constructor() : ProgressListener, ContentDownloadStateTracker { + + private val mainHandler = Handler(Looper.getMainLooper()) + private val states = mutableMapOf() + private val listeners = mutableMapOf>() + + override fun track(key: String, updateListener: ContentDownloadStateTracker.UpdateListener) { + val listeners = listeners.getOrPut(key) { ArrayList() } + if (!listeners.contains(updateListener)) { + listeners.add(updateListener) + } + } + + override fun unTrack(key: String, updateListener: ContentDownloadStateTracker.UpdateListener) { + listeners[key]?.apply { + remove(updateListener) + } + } + + override fun clear() { + listeners.clear() + } + +// private fun URL.toKey() = toString() + + override fun update(url: String, bytesRead: Long, contentLength: Long, done: Boolean) { + Timber.v("## DL Progress url:$url read:$bytesRead total:$contentLength done:$done") + listeners[url]?.forEach { + tryThis { + if (done) { + it.onDownloadStateUpdate(ContentDownloadStateTracker.State.Success) + } else { + it.onDownloadStateUpdate(ContentDownloadStateTracker.State.Downloading(bytesRead, contentLength, contentLength == -1L)) + } + } + } + } + + override fun error(url: String, errorCode: Int) { + Timber.v("## DL Progress Error code:$errorCode") + listeners[url]?.forEach { + tryThis { it.onDownloadStateUpdate(ContentDownloadStateTracker.State.Failure(errorCode)) } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/download/DownloadProgressInterceptor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/download/DownloadProgressInterceptor.kt new file mode 100644 index 0000000000..9de8a06276 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/download/DownloadProgressInterceptor.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.download + +import okhttp3.Interceptor +import okhttp3.Response +import javax.inject.Inject + +class DownloadProgressInterceptor @Inject constructor( + private val downloadStateTracker: DefaultContentDownloadStateTracker +) : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + val url = chain.request().url.toUrl() + val mxcURl = chain.request().header("matrix-sdk:mxc_URL") + + val request = chain.request().newBuilder() + .removeHeader("matrix-sdk:mxc_URL") + .build() + + val originalResponse = chain.proceed(request) + if (!originalResponse.isSuccessful) { + downloadStateTracker.error(mxcURl ?: url.toExternalForm(), originalResponse.code) + return originalResponse + } + val responseBody = originalResponse.body ?: return originalResponse + return originalResponse.newBuilder() + .body(ProgressResponseBody(responseBody, mxcURl ?: url.toExternalForm(), downloadStateTracker)) + .build() + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/download/ProgressResponseBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/download/ProgressResponseBody.kt new file mode 100644 index 0000000000..dc41b94321 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/download/ProgressResponseBody.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.download + +import okhttp3.MediaType +import okhttp3.ResponseBody +import okio.Buffer +import okio.BufferedSource +import okio.ForwardingSource +import okio.Source +import okio.buffer + +class ProgressResponseBody( + private val responseBody: ResponseBody, + private val chainUrl: String, + private val progressListener: ProgressListener) : ResponseBody() { + + private var bufferedSource: BufferedSource? = null + + override fun contentType(): MediaType? = responseBody.contentType() + override fun contentLength(): Long = responseBody.contentLength() + + override fun source(): BufferedSource { + if (bufferedSource == null) { + bufferedSource = source(responseBody.source()).buffer() + } + return bufferedSource!! + } + + fun source(source: Source): Source { + return object : ForwardingSource(source) { + var totalBytesRead = 0L + + override fun read(sink: Buffer, byteCount: Long): Long { + val bytesRead = super.read(sink, byteCount) + // read() returns the number of bytes read, or -1 if this source is exhausted. + totalBytesRead += if (bytesRead != -1L) bytesRead else 0L + progressListener.update(chainUrl, totalBytesRead, responseBody.contentLength(), bytesRead == -1L) + return bytesRead + } + } + } +} + +interface ProgressListener { + fun update(url: String, bytesRead: Long, contentLength: Long, done: Boolean) + fun error(url: String, errorCode: Int) +} diff --git a/matrix-sdk-android/src/main/res/xml/sdk_provider_paths.xml b/matrix-sdk-android/src/main/res/xml/sdk_provider_paths.xml new file mode 100644 index 0000000000..7c15e41df3 --- /dev/null +++ b/matrix-sdk-android/src/main/res/xml/sdk_provider_paths.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 20107c9b65..3ed0d95b71 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -204,7 +204,7 @@ + android:exported="false"> @@ -239,7 +239,7 @@ A media button receiver receives and helps translate hardware media playback buttons, such as those found on wired and wireless headsets, into the appropriate callbacks in your app. --> - + @@ -254,7 +254,7 @@ android:grantUriPermissions="true"> + android:resource="@xml/sdk_provider_paths" /> diff --git a/vector/src/main/java/im/vector/riotx/core/utils/ExternalApplicationsUtil.kt b/vector/src/main/java/im/vector/riotx/core/utils/ExternalApplicationsUtil.kt index 81be9620d0..0a20b9d7bf 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/ExternalApplicationsUtil.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/ExternalApplicationsUtil.kt @@ -35,6 +35,7 @@ import im.vector.riotx.BuildConfig import im.vector.riotx.R import okio.buffer import okio.sink +import okio.source import timber.log.Timber import java.io.File import java.text.SimpleDateFormat @@ -335,11 +336,16 @@ fun saveMedia(context: Context, file: File, title: String, mediaMimeType: String } } context.contentResolver.insert(externalContentUri, values)?.let { uri -> - context.contentResolver.openOutputStream(uri)?.use { outputStream -> - outputStream.sink().buffer().write(file.inputStream().use { it.readBytes() }) - return true + val source = file.inputStream().source().buffer() + context.contentResolver.openOutputStream(uri)?.sink()?.buffer()?.let { sink -> + source.use { input -> + sink.use { output -> + output.writeAll(input) + } + } } } + // TODO add notification? } else { @Suppress("DEPRECATION") Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE).also { mediaScanIntent -> diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt index 354591b618..c664e7e62c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt @@ -18,8 +18,8 @@ package im.vector.riotx.features.home.room.detail import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.room.model.message.MessageFileContent import im.vector.matrix.android.api.session.room.model.message.MessageStickerContent +import im.vector.matrix.android.api.session.room.model.message.MessageWithAttachmentContent import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotx.core.platform.VectorViewModelAction @@ -39,7 +39,7 @@ sealed class RoomDetailAction : VectorViewModelAction { data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val add: Boolean) : RoomDetailAction() data class NavigateToEvent(val eventId: String, val highlight: Boolean) : RoomDetailAction() object MarkAllAsRead : RoomDetailAction() - data class DownloadFile(val eventId: String, val messageFileContent: MessageFileContent) : RoomDetailAction() + data class DownloadOrOpen(val eventId: String, val messageFileContent: MessageWithAttachmentContent) : RoomDetailAction() data class HandleTombstoneEvent(val event: Event) : RoomDetailAction() object AcceptInvite : RoomDetailAction() object RejectInvite : RoomDetailAction() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 4688903e61..5c70502c6f 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -68,15 +68,14 @@ import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.api.session.room.model.Membership -import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent import im.vector.matrix.android.api.session.room.model.message.MessageContent -import im.vector.matrix.android.api.session.room.model.message.MessageFileContent import im.vector.matrix.android.api.session.room.model.message.MessageFormat import im.vector.matrix.android.api.session.room.model.message.MessageImageInfoContent import im.vector.matrix.android.api.session.room.model.message.MessageStickerContent import im.vector.matrix.android.api.session.room.model.message.MessageTextContent import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent +import im.vector.matrix.android.api.session.room.model.message.MessageWithAttachmentContent import im.vector.matrix.android.api.session.room.model.message.getFileUrl import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.timeline.Timeline @@ -98,7 +97,6 @@ import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.setTextOrHide import im.vector.riotx.core.extensions.showKeyboard import im.vector.riotx.core.extensions.trackItemsVisibilityChange -import im.vector.riotx.core.files.addEntryToDownloadManager import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.intent.getMimeTypeFromUri import im.vector.riotx.core.platform.VectorBaseFragment @@ -112,7 +110,6 @@ import im.vector.riotx.core.utils.KeyboardStateUtils import im.vector.riotx.core.utils.PERMISSIONS_FOR_AUDIO_IP_CALL import im.vector.riotx.core.utils.PERMISSIONS_FOR_VIDEO_IP_CALL import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES -import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_DOWNLOAD_FILE import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_INCOMING_URI import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_PICK_ATTACHMENT import im.vector.riotx.core.utils.TextUtils @@ -344,7 +341,7 @@ class RoomDetailFragment @Inject constructor( is RoomDetailViewEvents.DownloadFileState -> handleDownloadFileState(it) is RoomDetailViewEvents.JoinRoomCommandSuccess -> handleJoinedToAnotherRoom(it) is RoomDetailViewEvents.SendMessageResult -> renderSendMessageResult(it) - is RoomDetailViewEvents.ShowE2EErrorMessage -> displayE2eError(it.withHeldCode) + is RoomDetailViewEvents.ShowE2EErrorMessage -> displayE2eError(it.withHeldCode) RoomDetailViewEvents.DisplayPromptForIntegrationManager -> displayPromptForIntegrationManager() is RoomDetailViewEvents.OpenStickerPicker -> openStickerPicker(it) is RoomDetailViewEvents.DisplayEnableIntegrationsWarning -> displayDisabledIntegrationDialog() @@ -370,6 +367,21 @@ class RoomDetailFragment @Inject constructor( navigator.openStickerPicker(this, roomDetailArgs.roomId, event.widget) } + private fun startOpenFileIntent(action: RoomDetailViewEvents.OpenFile) { + if (action.uri != null) { + val intent = Intent(Intent.ACTION_VIEW).apply { + setDataAndTypeAndNormalize(action.uri, action.mimeType) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK) + } + + if (intent.resolveActivity(requireActivity().packageManager) != null) { + requireActivity().startActivity(intent) + } else { + requireActivity().toast(R.string.error_no_external_application_found) + } + } + } + private fun displayPromptForIntegrationManager() { // The Sticker picker widget is not installed yet. Propose the user to install it val builder = AlertDialog.Builder(requireContext()) @@ -487,21 +499,22 @@ class RoomDetailFragment @Inject constructor( val activity = requireActivity() if (action.throwable != null) { activity.toast(errorFormatter.toHumanReadable(action.throwable)) - } else if (action.file != null) { - addEntryToDownloadManager(activity, action.file, action.mimeType)?.let { - // This is a temporary solution to help users find downloaded files - // there is a better way to do that - // On android Q+ this method returns the file URI, on older - // it returns null, and the download manager handles the notification - notificationUtils.buildDownloadFileNotification( - it, - action.file.name ?: "file", - action.mimeType - ).let { notification -> - notificationUtils.showNotificationMessage("DL", action.file.absolutePath.hashCode(), notification) - } - } } +// else if (action.file != null) { +// addEntryToDownloadManager(activity, action.file, action.mimeType ?: "application/octet-stream")?.let { +// // This is a temporary solution to help users find downloaded files +// // there is a better way to do that +// // On android Q+ this method returns the file URI, on older +// // it returns null, and the download manager handles the notification +// notificationUtils.buildDownloadFileNotification( +// it, +// action.file.name ?: "file", +// action.mimeType ?: "application/octet-stream" +// ).let { notification -> +// notificationUtils.showNotificationMessage("DL", action.file.absolutePath.hashCode(), notification) +// } +// } +// } } private fun setupNotificationView() { @@ -680,6 +693,8 @@ class RoomDetailFragment @Inject constructor( } } } + // TODO why don't we call super here? + // super.onActivityResult(requestCode, resultCode, data) } // PRIVATE METHODS ***************************************************************************** @@ -1163,31 +1178,32 @@ class RoomDetailFragment @Inject constructor( navigator.openVideoViewer(requireActivity(), mediaData) } - override fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) { - val action = RoomDetailAction.DownloadFile(eventId, messageFileContent) - // We need WRITE_EXTERNAL permission - if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_DOWNLOAD_FILE)) { - showSnackWithMessage(getString(R.string.downloading_file, messageFileContent.getFileName())) - roomDetailViewModel.handle(action) - } else { - roomDetailViewModel.pendingAction = action - } - } +// override fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) { +// val isEncrypted = messageFileContent.encryptedFileInfo != null +// val action = RoomDetailAction.DownloadOrOpen(eventId, messageFileContent, isEncrypted) +// // We need WRITE_EXTERNAL permission +// // if (!isEncrypted || checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_DOWNLOAD_FILE)) { +// showSnackWithMessage(getString(R.string.downloading_file, messageFileContent.getFileName())) +// roomDetailViewModel.handle(action) +// // } else { +// // roomDetailViewModel.pendingAction = action +// // } +// } override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { if (allGranted(grantResults)) { when (requestCode) { - PERMISSION_REQUEST_CODE_DOWNLOAD_FILE -> { - val action = roomDetailViewModel.pendingAction - if (action != null) { - (action as? RoomDetailAction.DownloadFile) - ?.messageFileContent - ?.getFileName() - ?.let { showSnackWithMessage(getString(R.string.downloading_file, it)) } - roomDetailViewModel.pendingAction = null - roomDetailViewModel.handle(action) - } - } +// PERMISSION_REQUEST_CODE_DOWNLOAD_FILE -> { +// val action = roomDetailViewModel.pendingAction +// if (action != null) { +// (action as? RoomDetailAction.DownloadFile) +// ?.messageFileContent +// ?.getFileName() +// ?.let { showSnackWithMessage(getString(R.string.downloading_file, it)) } +// roomDetailViewModel.pendingAction = null +// roomDetailViewModel.handle(action) +// } +// } PERMISSION_REQUEST_CODE_INCOMING_URI -> { val pendingUri = roomDetailViewModel.pendingUri if (pendingUri != null) { @@ -1227,9 +1243,9 @@ class RoomDetailFragment @Inject constructor( } } - override fun onAudioMessageClicked(messageAudioContent: MessageAudioContent) { - vectorBaseActivity.notImplemented("open audio file") - } +// override fun onAudioMessageClicked(messageAudioContent: MessageAudioContent) { +// vectorBaseActivity.notImplemented("open audio file") +// } override fun onLoadMore(direction: Timeline.Direction) { roomDetailViewModel.handle(RoomDetailAction.LoadMoreTimelineEvents(direction)) @@ -1240,6 +1256,10 @@ class RoomDetailFragment @Inject constructor( is MessageVerificationRequestContent -> { roomDetailViewModel.handle(RoomDetailAction.ResumeVerification(informationData.eventId, null)) } + is MessageWithAttachmentContent -> { + val action = RoomDetailAction.DownloadOrOpen(informationData.eventId, messageContent) + roomDetailViewModel.handle(action) + } is EncryptedEventContent -> { roomDetailViewModel.handle(RoomDetailAction.TapOnFailedToDecrypt(informationData.eventId)) } @@ -1323,6 +1343,7 @@ class RoomDetailFragment @Inject constructor( action.eventId, action.messageContent.body, action.messageContent.getFileUrl(), + action.messageContent.mimeType, action.messageContent.encryptedFileInfo?.toElementToDecrypt(), object : MatrixCallback { override fun onSuccess(data: File) { @@ -1336,12 +1357,13 @@ class RoomDetailFragment @Inject constructor( private fun onSaveActionClicked(action: EventSharedAction.Save) { session.downloadFile( - FileService.DownloadMode.FOR_EXTERNAL_SHARE, - action.eventId, - action.messageContent.body, - action.messageContent.getFileUrl(), - action.messageContent.encryptedFileInfo?.toElementToDecrypt(), - object : MatrixCallback { + 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(), + callback = object : MatrixCallback { override fun onSuccess(data: File) { if (isAdded) { val saved = saveMedia( diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewEvents.kt index 6ed5373b58..b4c1f751bc 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewEvents.kt @@ -16,6 +16,7 @@ package im.vector.riotx.features.home.room.detail +import android.net.Uri import androidx.annotation.StringRes import im.vector.matrix.android.api.session.widgets.model.Widget import im.vector.matrix.android.internal.crypto.model.event.WithHeldCode @@ -45,11 +46,17 @@ sealed class RoomDetailViewEvents : VectorViewEvents { ) : RoomDetailViewEvents() data class DownloadFileState( - val mimeType: String, + val mimeType: String?, val file: File?, val throwable: Throwable? ) : RoomDetailViewEvents() + data class OpenFile( + val mimeType: String?, + val uri: Uri?, + val throwable: Throwable? + ) : RoomDetailViewEvents() + abstract class SendMessageResult : RoomDetailViewEvents() object DisplayPromptForIntegrationManager: RoomDetailViewEvents() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 9a79ea6a0a..b6a5009908 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -48,6 +48,7 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.OptionItem +import im.vector.matrix.android.api.session.room.model.message.getFileName import im.vector.matrix.android.api.session.room.model.message.getFileUrl import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsHelper @@ -243,7 +244,7 @@ class RoomDetailViewModel @AssistedInject constructor( is RoomDetailAction.EnterEditMode -> handleEditAction(action) is RoomDetailAction.EnterQuoteMode -> handleQuoteAction(action) is RoomDetailAction.EnterReplyMode -> handleReplyAction(action) - is RoomDetailAction.DownloadFile -> handleDownloadFile(action) + is RoomDetailAction.DownloadOrOpen -> handleOpenOrDownloadFile(action) is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action) is RoomDetailAction.HandleTombstoneEvent -> handleTombstoneEvent(action) is RoomDetailAction.ResendMessage -> handleResendEvent(action) @@ -858,30 +859,44 @@ class RoomDetailViewModel @AssistedInject constructor( } } - private fun handleDownloadFile(action: RoomDetailAction.DownloadFile) { - session.downloadFile( - FileService.DownloadMode.TO_EXPORT, - action.eventId, - action.messageFileContent.getFileName(), - action.messageFileContent.getFileUrl(), - action.messageFileContent.encryptedFileInfo?.toElementToDecrypt(), - object : MatrixCallback { - override fun onSuccess(data: File) { - _viewEvents.post(RoomDetailViewEvents.DownloadFileState( - action.messageFileContent.getMimeType(), - data, - null - )) - } + private fun handleOpenOrDownloadFile(action: RoomDetailAction.DownloadOrOpen) { + val mxcUrl = action.messageFileContent.getFileUrl() + val isDownloaded = mxcUrl?.let { session.isFileInCache(it, action.messageFileContent.mimeType) } ?: false + if (isDownloaded) { + // we can open it + session.getTemporarySharableURI(mxcUrl!!, action.messageFileContent.mimeType)?.let { uri -> + _viewEvents.post(RoomDetailViewEvents.OpenFile( + action.messageFileContent.mimeType, + uri, + null + )) + } + } else { + session.downloadFile( + FileService.DownloadMode.FOR_INTERNAL_USE, + action.eventId, + action.messageFileContent.getFileName(), + action.messageFileContent.mimeType, + mxcUrl, + action.messageFileContent.encryptedFileInfo?.toElementToDecrypt(), + object : MatrixCallback { + override fun onSuccess(data: File) { + _viewEvents.post(RoomDetailViewEvents.DownloadFileState( + action.messageFileContent.mimeType, + data, + null + )) + } - override fun onFailure(failure: Throwable) { - _viewEvents.post(RoomDetailViewEvents.DownloadFileState( - action.messageFileContent.getMimeType(), - null, - failure - )) - } - }) + override fun onFailure(failure: Throwable) { + _viewEvents.post(RoomDetailViewEvents.DownloadFileState( + action.messageFileContent.mimeType, + null, + failure + )) + } + }) + } } private fun handleNavigateToEvent(action: RoomDetailAction.NavigateToEvent) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt index 095340caee..dbddb5cac7 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt @@ -27,6 +27,7 @@ import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.VisibilityState import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent import im.vector.matrix.android.api.session.room.model.message.MessageFileContent +import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageImageInfoContent import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent import im.vector.matrix.android.api.session.room.timeline.Timeline @@ -74,8 +75,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View) fun onImageMessageClicked(messageImageContent: MessageImageInfoContent, mediaData: ImageContentRenderer.Data, view: View) fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) - fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) - fun onAudioMessageClicked(messageAudioContent: MessageAudioContent) +// fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) +// fun onAudioMessageClicked(messageAudioContent: MessageAudioContent) fun onEditedDecorationClicked(informationData: MessageInformationData) // TODO move all callbacks to this? diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 96abe1ff40..4e37f28793 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -57,6 +57,7 @@ import im.vector.riotx.core.utils.containsOnlyEmojis import im.vector.riotx.core.utils.isLocalFile import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider +import im.vector.riotx.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder import im.vector.riotx.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformationDataFactory import im.vector.riotx.features.home.room.detail.timeline.helper.MessageItemAttributesFactory @@ -99,6 +100,7 @@ class MessageItemFactory @Inject constructor( private val messageInformationDataFactory: MessageInformationDataFactory, private val messageItemAttributesFactory: MessageItemAttributesFactory, private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder, + private val contentDownloadStateTrackerBinder: ContentDownloadStateTrackerBinder, private val defaultItemFactory: DefaultItemFactory, private val noticeItemFactory: NoticeItemFactory, private val avatarSizeProvider: AvatarSizeProvider, @@ -140,8 +142,8 @@ class MessageItemFactory @Inject constructor( is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessageFileContent -> buildFileMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageFileContent -> buildFileMessageItem(messageContent, highlight, attributes) + is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, attributes) is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageOptionsContent -> buildOptionsMessageItem(messageContent, informationData, highlight, callback, attributes) is MessagePollResponseContent -> noticeItemFactory.create(event, highlight, callback) @@ -184,20 +186,22 @@ class MessageItemFactory @Inject constructor( @Suppress("UNUSED_PARAMETER") informationData: MessageInformationData, highlight: Boolean, - callback: TimelineEventController.Callback?, +// callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes): MessageFileItem? { return MessageFileItem_() .attributes(attributes) .izLocalFile(messageContent.getFileUrl().isLocalFile()) + .mxcUrl(messageContent.getFileUrl() ?: "") .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) + .contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder) .highlighted(highlight) .leftGuideline(avatarSizeProvider.leftGuideline) .filename(messageContent.body) - .iconRes(R.drawable.filetype_audio) - .clickListener( - DebouncedClickListener(View.OnClickListener { - callback?.onAudioMessageClicked(messageContent) - })) + .iconRes(R.drawable.ic_paperclip) +// .clickListener( +// DebouncedClickListener(View.OnClickListener { +// callback?.onAudioMessageClicked(messageContent) +// })) } private fun buildVerificationRequestMessageItem(messageContent: MessageVerificationRequestContent, @@ -245,22 +249,24 @@ class MessageItemFactory @Inject constructor( } private fun buildFileMessageItem(messageContent: MessageFileContent, - informationData: MessageInformationData, +// informationData: MessageInformationData, highlight: Boolean, - callback: TimelineEventController.Callback?, +// callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes): MessageFileItem? { return MessageFileItem_() .attributes(attributes) .leftGuideline(avatarSizeProvider.leftGuideline) .izLocalFile(messageContent.getFileUrl().isLocalFile()) + .mxcUrl(messageContent.getFileUrl() ?: "") .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) + .contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder) .highlighted(highlight) .filename(messageContent.body) - .iconRes(R.drawable.filetype_attachment) - .clickListener( - DebouncedClickListener(View.OnClickListener { - callback?.onFileMessageClicked(informationData.eventId, messageContent) - })) + .iconRes(R.drawable.ic_paperclip) +// .clickListener( +// DebouncedClickListener(View.OnClickListener { +// callback?.onFileMessageClicked(informationData.eventId, messageContent) +// })) } private fun buildNotHandledMessageItem(messageContent: MessageContent, @@ -282,6 +288,7 @@ class MessageItemFactory @Inject constructor( val data = ImageContentRenderer.Data( eventId = informationData.eventId, filename = messageContent.body, + mimeType = messageContent.mimeType, url = messageContent.getFileUrl(), elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(), height = messageContent.info?.height, @@ -318,6 +325,7 @@ class MessageItemFactory @Inject constructor( val thumbnailData = ImageContentRenderer.Data( eventId = informationData.eventId, filename = messageContent.body, + mimeType = messageContent.mimeType, url = messageContent.videoInfo?.thumbnailFile?.url ?: messageContent.videoInfo?.thumbnailUrl, elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(), @@ -330,6 +338,7 @@ class MessageItemFactory @Inject constructor( val videoData = VideoContentRenderer.Data( eventId = informationData.eventId, filename = messageContent.body, + mimeType = messageContent.mimeType, url = messageContent.getFileUrl(), elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(), thumbnailMediaData = thumbnailData diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/ContentDownloadStateTrackerBinder.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/ContentDownloadStateTrackerBinder.kt new file mode 100644 index 0000000000..47ffe47132 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/ContentDownloadStateTrackerBinder.kt @@ -0,0 +1,112 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.home.room.detail.timeline.helper + +import im.vector.matrix.android.internal.session.download.ContentDownloadStateTracker +import im.vector.riotx.R +import im.vector.riotx.core.di.ActiveSessionHolder +import im.vector.riotx.core.di.ScreenScope +import im.vector.riotx.core.error.ErrorFormatter +import im.vector.riotx.features.home.room.detail.timeline.MessageColorProvider +import im.vector.riotx.features.home.room.detail.timeline.item.MessageFileItem +import javax.inject.Inject + +@ScreenScope +class ContentDownloadStateTrackerBinder @Inject constructor(private val activeSessionHolder: ActiveSessionHolder, + private val messageColorProvider: MessageColorProvider, + private val errorFormatter: ErrorFormatter) { + + private val updateListeners = mutableMapOf() + + fun bind(mxcUrl: String, + holder: MessageFileItem.Holder) { + activeSessionHolder.getSafeActiveSession()?.also { session -> + val downloadStateTracker = session.contentDownloadProgressTracker() + val updateListener = ContentDownloadUpdater(holder, messageColorProvider, errorFormatter) + updateListeners[mxcUrl] = updateListener + downloadStateTracker.track(mxcUrl, updateListener) + } + } + + fun unbind(mxcUrl: String) { + activeSessionHolder.getSafeActiveSession()?.also { session -> + val downloadStateTracker = session.contentDownloadProgressTracker() + updateListeners[mxcUrl]?.also { + downloadStateTracker.unTrack(mxcUrl, it) + } + } + } + + fun clear() { + activeSessionHolder.getSafeActiveSession()?.also { + it.contentUploadProgressTracker().clear() + } + } +} + +private class ContentDownloadUpdater(private val holder: MessageFileItem.Holder, + private val messageColorProvider: MessageColorProvider, + private val errorFormatter: ErrorFormatter) : ContentDownloadStateTracker.UpdateListener { + + override fun onDownloadStateUpdate(state: ContentDownloadStateTracker.State) { + when (state) { + ContentDownloadStateTracker.State.Idle -> handleIdle() + is ContentDownloadStateTracker.State.Downloading -> handleProgress(state) + ContentDownloadStateTracker.State.Decrypting -> handleDecrypting() + ContentDownloadStateTracker.State.Success -> handleSuccess() + is ContentDownloadStateTracker.State.Failure -> handleFailure() + } + } + + // avoid blink effect when setting icon + private var hasDLResource = false + + private fun handleIdle() { + holder.fileDownloadProgress.progress = 0 + holder.fileDownloadProgress.isIndeterminate = false + } + + private fun handleDecrypting() { + holder.fileDownloadProgress.isIndeterminate = true + } + + private fun handleProgress(state: ContentDownloadStateTracker.State.Downloading) { + doHandleProgress(state.current, state.total) + } + + private fun doHandleProgress(current: Long, total: Long) { + val percent = 100L * (current.toFloat() / total.toFloat()) + holder.fileDownloadProgress.isIndeterminate = false + holder.fileDownloadProgress.progress = percent.toInt() + if (!hasDLResource) { + holder.fileImageView.setImageResource(R.drawable.ic_download) + hasDLResource = true + } + } + + private fun handleFailure() { + holder.fileDownloadProgress.isIndeterminate = false + holder.fileDownloadProgress.progress = 0 + holder.fileImageView.setImageResource(R.drawable.ic_close_round) + } + + private fun handleSuccess() { + holder.fileDownloadProgress.isIndeterminate = false + holder.fileDownloadProgress.progress = 100 + holder.fileImageView.setImageResource(R.drawable.ic_paperclip) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index 44644fb942..b001e192c5 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -21,9 +21,13 @@ package im.vector.riotx.features.home.room.detail.timeline.helper import im.vector.matrix.android.api.extensions.orFalse import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.isFileMessage import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.ReferencesAggregatedContent +import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent +import im.vector.matrix.android.api.session.room.model.message.MessageWithAttachmentContent +import im.vector.matrix.android.api.session.room.model.message.getFileUrl import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent @@ -111,7 +115,8 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses ReferencesInfoData(verificationState) }, sentByMe = event.root.senderId == session.myUserId, - e2eDecoration = e2eDecoration + e2eDecoration = e2eDecoration, + isDowloaded = isDownloaded ) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageFileItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageFileItem.kt index ea52df2bfb..5f063408e5 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageFileItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageFileItem.kt @@ -17,15 +17,16 @@ package im.vector.riotx.features.home.room.detail.timeline.item import android.graphics.Paint -import android.view.View import android.view.ViewGroup import android.widget.ImageView +import android.widget.ProgressBar import android.widget.TextView import androidx.annotation.DrawableRes import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R +import im.vector.riotx.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder import im.vector.riotx.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder @EpoxyModelClass(layout = R.layout.item_timeline_event_base) @@ -33,16 +34,26 @@ abstract class MessageFileItem : AbsMessageItem() { @EpoxyAttribute var filename: CharSequence = "" + + @EpoxyAttribute + var mxcUrl: String = "" + @EpoxyAttribute @DrawableRes var iconRes: Int = 0 - @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) - var clickListener: View.OnClickListener? = null + +// @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) +// var clickListener: View.OnClickListener? = null + @EpoxyAttribute var izLocalFile = false + @EpoxyAttribute lateinit var contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder + @EpoxyAttribute + lateinit var contentDownloadStateTrackerBinder: ContentDownloadStateTrackerBinder + override fun bind(holder: Holder) { super.bind(holder) renderSendState(holder.fileLayout, holder.filenameView) @@ -51,15 +62,30 @@ abstract class MessageFileItem : AbsMessageItem() { } else { holder.progressLayout.isVisible = false } + if (!attributes.informationData.isDowloaded) { + contentDownloadStateTrackerBinder.bind(mxcUrl, holder) + } holder.filenameView.text = filename - holder.fileImageView.setImageResource(iconRes) - holder.filenameView.setOnClickListener(clickListener) + if (attributes.informationData.isDowloaded) { + holder.fileImageView.setImageResource(iconRes) + holder.fileDownloadProgress.progress = 100 + } else { + holder.fileImageView.setImageResource(R.drawable.ic_download) + holder.fileDownloadProgress.progress = 0 + } +// holder.view.setOnClickListener(clickListener) + + holder.filenameView.setOnClickListener(attributes.itemClickListener) + holder.filenameView.setOnLongClickListener(attributes.itemLongClickListener) + holder.fileImageWrapper.setOnClickListener(attributes.itemClickListener) + holder.fileImageWrapper.setOnLongClickListener(attributes.itemLongClickListener) holder.filenameView.paintFlags = (holder.filenameView.paintFlags or Paint.UNDERLINE_TEXT_FLAG) } override fun unbind(holder: Holder) { super.unbind(holder) contentUploadStateTrackerBinder.unbind(attributes.informationData.eventId) + contentDownloadStateTrackerBinder.unbind(mxcUrl) } override fun getViewType() = STUB_ID @@ -67,7 +93,9 @@ abstract class MessageFileItem : AbsMessageItem() { class Holder : AbsMessageItem.Holder(STUB_ID) { val progressLayout by bind(R.id.messageFileUploadProgressLayout) val fileLayout by bind(R.id.messageFileLayout) - val fileImageView by bind(R.id.messageFileImageView) + val fileImageView by bind(R.id.messageFileIconView) + val fileImageWrapper by bind(R.id.messageFileImageView) + val fileDownloadProgress by bind(R.id.messageFileProgressbar) val filenameView by bind(R.id.messageFilenameView) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt index 088577d03a..9638a150c5 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt @@ -41,7 +41,9 @@ data class MessageInformationData( val readReceipts: List = emptyList(), val referencesInfoData: ReferencesInfoData? = null, val sentByMe : Boolean, - val e2eDecoration: E2EDecoration = E2EDecoration.NONE + val e2eDecoration: E2EDecoration = E2EDecoration.NONE, + // used for file messages + val isDowloaded: Boolean = true ) : Parcelable { val matrixItem: MatrixItem diff --git a/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt b/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt index ab047fba0d..eeeb55ed15 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt @@ -49,6 +49,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: data class Data( val eventId: String, val filename: String, + val mimeType: String?, val url: String?, val elementToDecrypt: ElementToDecrypt?, val height: Int?, diff --git a/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt index ca6510a897..00f47b6a28 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt @@ -137,6 +137,7 @@ class ImageMediaViewerActivity : VectorBaseActivity() { FileService.DownloadMode.FOR_EXTERNAL_SHARE, mediaData.eventId, mediaData.filename, + mediaData.mimeType, mediaData.url, mediaData.elementToDecrypt, object : MatrixCallback { diff --git a/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt b/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt index 833f795ecb..0d6ac176c1 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt @@ -40,6 +40,7 @@ class VideoContentRenderer @Inject constructor(private val activeSessionHolder: data class Data( val eventId: String, val filename: String, + val mimeType: String?, val url: String?, val elementToDecrypt: ElementToDecrypt?, val thumbnailMediaData: ImageContentRenderer.Data @@ -66,12 +67,13 @@ class VideoContentRenderer @Inject constructor(private val activeSessionHolder: activeSessionHolder.getActiveSession() .downloadFile( - FileService.DownloadMode.FOR_INTERNAL_USE, - data.eventId, - data.filename, - data.url, - data.elementToDecrypt, - object : MatrixCallback { + downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE, + id = data.eventId, + fileName = data.filename, + mimeType = null, + url = data.url, + elementToDecrypt = data.elementToDecrypt, + callback = object : MatrixCallback { override fun onSuccess(data: File) { thumbnailView.isVisible = false loadingView.isVisible = false @@ -104,12 +106,13 @@ class VideoContentRenderer @Inject constructor(private val activeSessionHolder: activeSessionHolder.getActiveSession() .downloadFile( - FileService.DownloadMode.FOR_INTERNAL_USE, - data.eventId, - data.filename, - data.url, - null, - object : MatrixCallback { + downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE, + id = data.eventId, + fileName = data.filename, + mimeType = data.mimeType, + url = data.url, + elementToDecrypt = null, + callback = object : MatrixCallback { override fun onSuccess(data: File) { thumbnailView.isVisible = false loadingView.isVisible = false diff --git a/vector/src/main/java/im/vector/riotx/features/media/VideoMediaViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/VideoMediaViewerActivity.kt index 6985278ad0..1e8a2d8a72 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/VideoMediaViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/VideoMediaViewerActivity.kt @@ -82,6 +82,7 @@ class VideoMediaViewerActivity : VectorBaseActivity() { FileService.DownloadMode.FOR_EXTERNAL_SHARE, mediaData.eventId, mediaData.filename, + mediaData.mimeType, mediaData.url, mediaData.elementToDecrypt, object : MatrixCallback { diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewModel.kt index 952e80c035..6037d17a84 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewModel.kt @@ -137,12 +137,13 @@ class RoomUploadsViewModel @AssistedInject constructor( try { val file = awaitCallback { session.downloadFile( - FileService.DownloadMode.FOR_EXTERNAL_SHARE, - action.uploadEvent.eventId, - action.uploadEvent.contentWithAttachmentContent.body, - action.uploadEvent.contentWithAttachmentContent.getFileUrl(), - action.uploadEvent.contentWithAttachmentContent.encryptedFileInfo?.toElementToDecrypt(), - it + 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(), + callback = it ) } _viewEvents.post(RoomUploadsViewEvents.FileReadyForSharing(file)) @@ -161,6 +162,7 @@ class RoomUploadsViewModel @AssistedInject constructor( action.uploadEvent.eventId, action.uploadEvent.contentWithAttachmentContent.body, action.uploadEvent.contentWithAttachmentContent.getFileUrl(), + action.uploadEvent.contentWithAttachmentContent.mimeType, action.uploadEvent.contentWithAttachmentContent.encryptedFileInfo?.toElementToDecrypt(), it) } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsMediaController.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsMediaController.kt index cd3e401dc5..72e4cb6d06 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsMediaController.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsMediaController.kt @@ -115,6 +115,7 @@ class UploadsMediaController @Inject constructor( eventId = eventId, filename = messageContent.body, url = messageContent.getFileUrl(), + mimeType = messageContent.mimeType, elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(), height = messageContent.info?.height, maxHeight = itemSize, @@ -129,6 +130,7 @@ class UploadsMediaController @Inject constructor( val thumbnailData = ImageContentRenderer.Data( eventId = eventId, filename = messageContent.body, + mimeType = messageContent.mimeType, url = messageContent.videoInfo?.thumbnailFile?.url ?: messageContent.videoInfo?.thumbnailUrl, elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(), height = messageContent.videoInfo?.height, @@ -140,6 +142,7 @@ class UploadsMediaController @Inject constructor( return VideoContentRenderer.Data( eventId = eventId, filename = messageContent.body, + mimeType = messageContent.mimeType, url = messageContent.getFileUrl(), elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(), thumbnailMediaData = thumbnailData diff --git a/vector/src/main/res/drawable/file_progress_bar.xml b/vector/src/main/res/drawable/file_progress_bar.xml new file mode 100644 index 0000000000..4c96aaebf6 --- /dev/null +++ b/vector/src/main/res/drawable/file_progress_bar.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/drawable/ic_paperclip.xml b/vector/src/main/res/drawable/ic_paperclip.xml new file mode 100644 index 0000000000..57405db7a5 --- /dev/null +++ b/vector/src/main/res/drawable/ic_paperclip.xml @@ -0,0 +1,13 @@ + + + diff --git a/vector/src/main/res/layout/item_timeline_event_file_stub.xml b/vector/src/main/res/layout/item_timeline_event_file_stub.xml index 1c185ca973..a15de8bd34 100644 --- a/vector/src/main/res/layout/item_timeline_event_file_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_file_stub.xml @@ -12,22 +12,33 @@ android:id="@+id/messageFilee2eIcon" android:layout_width="14dp" android:layout_height="14dp" - android:src="@drawable/e2e_verified" + android:src="@drawable/ic_shield_black" android:visibility="gone" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:visibility="visible" /> - + app:layout_constraintTop_toTopOf="parent"> + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/colors_riotx.xml b/vector/src/main/res/values/colors_riotx.xml index 348deb57bb..a9cb32c3fd 100644 --- a/vector/src/main/res/values/colors_riotx.xml +++ b/vector/src/main/res/values/colors_riotx.xml @@ -21,6 +21,7 @@ #FFFF4B55 #FF61708B + #1E61708B #FF368BD6 #FF03b381 diff --git a/vector/src/main/res/values/styles_riot.xml b/vector/src/main/res/values/styles_riot.xml index c4b42fe4fe..64da6ee48c 100644 --- a/vector/src/main/res/values/styles_riot.xml +++ b/vector/src/main/res/values/styles_riot.xml @@ -378,4 +378,11 @@ 8dp + + \ No newline at end of file