diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt index 0397b514..06880ceb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt @@ -26,12 +26,14 @@ import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult import im.vector.matrix.android.internal.crypto.NewSessionListener +import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody +import java.io.File interface CryptoService { @@ -103,6 +105,13 @@ interface CryptoService { fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback) + /** + * Decrypt a file. + * Result will be a decrypted file, stored in the cache folder. id parameter will be used to create a sub folder to avoid name collision. + * You can pass the eventId + */ + fun decryptFile(id: String, filename: String, url: String, elementToDecrypt: ElementToDecrypt, callback: MatrixCallback) + fun getEncryptionAlgorithm(roomId: String): String? fun shouldEncryptForInvitedMembers(roomId: String): Boolean diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoManager.kt index 667f1446..99911749 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoManager.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoManager.kt @@ -48,6 +48,7 @@ import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAct import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting import im.vector.matrix.android.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory import im.vector.matrix.android.internal.crypto.algorithms.olm.MXOlmEncryptionFactory +import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo @@ -78,6 +79,7 @@ import im.vector.matrix.android.internal.util.fetchCopied import kotlinx.coroutines.* import org.matrix.olm.OlmManager import timber.log.Timber +import java.io.File import java.util.* import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject @@ -112,6 +114,8 @@ internal class CryptoManager @Inject constructor( private val keysBackup: KeysBackup, // private val objectSigner: ObjectSigner, + // File decryptor + private val fileDecryptor: FileDecryptor, // private val oneTimeKeysUploader: OneTimeKeysUploader, // @@ -607,6 +611,10 @@ internal class CryptoManager @Inject constructor( } } + override fun decryptFile(id: String, filename: String, url: String, elementToDecrypt: ElementToDecrypt, callback: MatrixCallback) { + fileDecryptor.decryptFile(id, filename, url, elementToDecrypt, callback) + } + /** * Decrypt an event * diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/FileDecryptor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/FileDecryptor.kt new file mode 100644 index 00000000..bcdd36f8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/FileDecryptor.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto + +import android.content.Context +import arrow.core.Try +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.auth.data.SessionParams +import im.vector.matrix.android.api.session.content.ContentUrlResolver +import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt +import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments +import im.vector.matrix.android.internal.extensions.foldToCallback +import im.vector.matrix.android.internal.session.SessionScope +import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers +import im.vector.matrix.android.internal.util.md5 +import im.vector.matrix.android.internal.util.writeToFile +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import okhttp3.Request +import timber.log.Timber +import java.io.File +import java.io.IOException +import javax.inject.Inject + +@SessionScope +internal class FileDecryptor @Inject constructor(private val context: Context, + private val sessionParams: SessionParams, + private val contentUrlResolver: ContentUrlResolver, + private val coroutineDispatchers: MatrixCoroutineDispatchers) { + + val okHttpClient = OkHttpClient() + + fun decryptFile(id: String, + fileName: String, + url: String, + elementToDecrypt: ElementToDecrypt, + callback: MatrixCallback) { + GlobalScope.launch(coroutineDispatchers.main) { + withContext(coroutineDispatchers.io) { + Try { + // Create dir tree: + // /DF/// + val tmpFolderRoot = File(context.cacheDir, "DF") + val tmpFolderUser = File(tmpFolderRoot, sessionParams.credentials.userId.md5()) + val tmpFolder = File(tmpFolderUser, id.md5()) + + if (!tmpFolder.exists()) { + tmpFolder.mkdirs() + } + + File(tmpFolder, fileName) + }.map { destFile -> + if (!destFile.exists()) { + Try { + Timber.v("## decrypt file") + + val resolvedUrl = contentUrlResolver.resolveFullSize(url) ?: throw IllegalArgumentException("url is null") + + val request = Request.Builder() + .url(resolvedUrl) + .build() + + val response = okHttpClient.newCall(request).execute() + val inputStream = response.body()?.byteStream() + Timber.v("Response size ${response.body()?.contentLength()} - Stream available: ${inputStream?.available()}") + if (!response.isSuccessful) { + throw IOException() + } + + MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt) ?: throw IllegalStateException("Decryption error") + } + .map { inputStream -> + writeToFile(inputStream, destFile) + } + } + + destFile + } + } + .foldToCallback(callback) + } + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MXEncryptedAttachments.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MXEncryptedAttachments.kt index 02c8931f..d8c62709 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MXEncryptedAttachments.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MXEncryptedAttachments.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.internal.crypto.attachments -import android.text.TextUtils import android.util.Base64 import arrow.core.Try import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo @@ -198,7 +197,7 @@ object MXEncryptedAttachments { val currentDigestValue = base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT)) - if (!TextUtils.equals(elementToDecrypt.sha256, currentDigestValue)) { + if (elementToDecrypt.sha256 != currentDigestValue) { Timber.e("## decryptAttachment() : Digest value mismatch") outStream.close() return null diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/FileSaver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/FileSaver.kt new file mode 100644 index 00000000..9654f5e0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/FileSaver.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.util + +import androidx.annotation.WorkerThread +import okio.Okio +import java.io.File +import java.io.InputStream + +/** + * Save an input stream to a file with Okio + */ +@WorkerThread +fun writeToFile(inputStream: InputStream, outputFile: File) { + val source = Okio.buffer(Okio.source(inputStream)) + val sink = Okio.buffer(Okio.sink(outputFile)) + + source.use { input -> + sink.use { output -> + output.writeAll(input) + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/core/files/FileSaver.kt b/vector/src/main/java/im/vector/riotx/core/files/FileSaver.kt index 23798587..d6b0056c 100644 --- a/vector/src/main/java/im/vector/riotx/core/files/FileSaver.kt +++ b/vector/src/main/java/im/vector/riotx/core/files/FileSaver.kt @@ -23,6 +23,7 @@ import arrow.core.Try import okio.Okio import timber.log.Timber import java.io.File +import java.io.InputStream /** * Save a string to a file with Okio 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 92ec26dc..17328ca0 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 @@ -29,14 +29,7 @@ import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan import im.vector.matrix.android.api.session.events.model.RelationType import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary -import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent -import im.vector.matrix.android.api.session.room.model.message.MessageContent -import im.vector.matrix.android.api.session.room.model.message.MessageEmoteContent -import im.vector.matrix.android.api.session.room.model.message.MessageFileContent -import im.vector.matrix.android.api.session.room.model.message.MessageImageContent -import im.vector.matrix.android.api.session.room.model.message.MessageNoticeContent -import im.vector.matrix.android.api.session.room.model.message.MessageTextContent -import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent +import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt @@ -87,9 +80,9 @@ class MessageItemFactory @Inject constructor( val messageContent: MessageContent = event.annotations?.editSummary?.aggregatedContent?.toModel() - ?: event.root.getClearContent().toModel() - ?: //Malformed content, we should echo something on screen - return DefaultItem_().text(stringProvider.getString(R.string.malformed_message)) + ?: event.root.getClearContent().toModel() + ?: //Malformed content, we should echo something on screen + return DefaultItem_().text(stringProvider.getString(R.string.malformed_message)) if (messageContent.relatesTo?.type == RelationType.REPLACE) { // ignore replace event, the targeted id is already edited @@ -99,16 +92,16 @@ class MessageItemFactory @Inject constructor( // val ev = all.toModel() return when (messageContent) { is MessageEmoteContent -> buildEmoteMessageItem(messageContent, - informationData, - event.annotations?.editSummary, - highlight, - callback) + informationData, + event.annotations?.editSummary, + highlight, + callback) is MessageTextContent -> buildTextMessageItem(event.sendState, - messageContent, - informationData, - event.annotations?.editSummary, - highlight, - callback + messageContent, + informationData, + event.annotations?.editSummary, + highlight, + callback ) is MessageImageContent -> buildImageMessageItem(messageContent, informationData, highlight, callback) is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback) @@ -142,7 +135,7 @@ class MessageItemFactory @Inject constructor( })) .longClickListener { view -> return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false + ?: false } } @@ -165,7 +158,7 @@ class MessageItemFactory @Inject constructor( })) .longClickListener { view -> return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false + ?: false } .clickListener( DebouncedClickListener(View.OnClickListener { _ -> @@ -218,7 +211,7 @@ class MessageItemFactory @Inject constructor( })) .longClickListener { view -> return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false + ?: false } } @@ -239,8 +232,10 @@ class MessageItemFactory @Inject constructor( ) val videoData = VideoContentRenderer.Data( + eventId = informationData.eventId, filename = messageContent.body, - videoUrl = messageContent.url, + url = messageContent.encryptedFileInfo?.url ?: messageContent.url, + elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(), thumbnailMediaData = thumbnailData ) @@ -262,7 +257,7 @@ class MessageItemFactory @Inject constructor( .clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view) } .longClickListener { view -> return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false + ?: false } } @@ -302,7 +297,7 @@ class MessageItemFactory @Inject constructor( })) .longClickListener { view -> return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false + ?: false } } @@ -334,9 +329,9 @@ class MessageItemFactory @Inject constructor( //nop } }, - editStart, - editEnd, - Spanned.SPAN_INCLUSIVE_EXCLUSIVE) + editStart, + editEnd, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE) return spannable } @@ -372,7 +367,7 @@ class MessageItemFactory @Inject constructor( })) .longClickListener { view -> return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false + ?: false } } @@ -408,7 +403,7 @@ class MessageItemFactory @Inject constructor( })) .longClickListener { view -> return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false + ?: false } } 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 412043e8..4d68d869 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 @@ -100,7 +100,6 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: return } - // TODO DECRYPT_FILE Decrypt file imageView.showImage( Uri.parse(thumbnail), Uri.parse(fullSize) 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 5cba0084..c369f775 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 @@ -18,26 +18,87 @@ package im.vector.riotx.features.media import android.os.Parcelable import android.widget.ImageView +import android.widget.ProgressBar +import android.widget.TextView import android.widget.VideoView +import androidx.core.view.isVisible +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt +import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder +import im.vector.riotx.core.error.ErrorFormatter import kotlinx.android.parcel.Parcelize +import timber.log.Timber +import java.io.File import javax.inject.Inject -class VideoContentRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder){ +class VideoContentRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder, + private val errorFormatter: ErrorFormatter) { - // TODO DECRYPT_FILE Encrypted data @Parcelize data class Data( + val eventId: String, val filename: String, - val videoUrl: String?, + val url: String?, + val elementToDecrypt: ElementToDecrypt?, val thumbnailMediaData: ImageContentRenderer.Data ) : Parcelable - fun render(data: Data, thumbnailView: ImageView, videoView: VideoView) { + fun render(data: Data, + thumbnailView: ImageView, + loadingView: ProgressBar, + videoView: VideoView, + errorView: TextView) { val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver() - val resolvedUrl = contentUrlResolver.resolveFullSize(data.videoUrl) - videoView.setVideoPath(resolvedUrl) - videoView.start() + + if (data.elementToDecrypt != null) { + Timber.v("Decrypt video") + videoView.isVisible = false + + if (data.url == null) { + loadingView.isVisible = false + errorView.isVisible = true + errorView.setText(R.string.unknown_error) + } else { + thumbnailView.isVisible = true + loadingView.isVisible = true + + activeSessionHolder.getActiveSession() + .decryptFile(data.eventId, + data.filename, + data.url, + data.elementToDecrypt, + object : MatrixCallback { + override fun onSuccess(data: File) { + thumbnailView.isVisible = false + loadingView.isVisible = false + videoView.isVisible = true + + videoView.setVideoPath(data.path) + videoView.start() + } + + override fun onFailure(failure: Throwable) { + loadingView.isVisible = false + errorView.isVisible = true + errorView.text = errorFormatter.toHumanReadable(failure) + } + }) + } + } else { + thumbnailView.isVisible = false + loadingView.isVisible = false + + val resolvedUrl = contentUrlResolver.resolveFullSize(data.url) + + if (resolvedUrl == null) { + errorView.isVisible = true + errorView.setText(R.string.unknown_error) + } else { + videoView.setVideoPath(resolvedUrl) + videoView.start() + } + } } } \ No newline at end of file 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 e8e4f213..093e257c 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 @@ -28,6 +28,7 @@ import javax.inject.Inject class VideoMediaViewerActivity : VectorBaseActivity() { + @Inject lateinit var imageContentRenderer: ImageContentRenderer @Inject lateinit var videoContentRenderer: VideoContentRenderer override fun injectWith(injector: ScreenComponent) { @@ -38,12 +39,10 @@ class VideoMediaViewerActivity : VectorBaseActivity() { super.onCreate(savedInstanceState) setContentView(im.vector.riotx.R.layout.activity_video_media_viewer) val mediaData = intent.getParcelableExtra(EXTRA_MEDIA_DATA) - if (mediaData.videoUrl.isNullOrEmpty()) { - finish() - } else { - configureToolbar(videoMediaViewerToolbar, mediaData) - videoContentRenderer.render(mediaData, videoMediaViewerThumbnailView, videoMediaViewerVideoView) - } + + configureToolbar(videoMediaViewerToolbar, mediaData) + imageContentRenderer.render(mediaData.thumbnailMediaData, ImageContentRenderer.Mode.FULL_SIZE, videoMediaViewerThumbnailView) + videoContentRenderer.render(mediaData, videoMediaViewerThumbnailView, videoMediaViewerLoading, videoMediaViewerVideoView, videoMediaViewerErrorView) } private fun configureToolbar(toolbar: Toolbar, mediaData: VideoContentRenderer.Data) { diff --git a/vector/src/main/res/layout/activity_video_media_viewer.xml b/vector/src/main/res/layout/activity_video_media_viewer.xml index e1f243b5..f21e6daf 100644 --- a/vector/src/main/res/layout/activity_video_media_viewer.xml +++ b/vector/src/main/res/layout/activity_video_media_viewer.xml @@ -15,6 +15,7 @@ --> @@ -32,12 +33,35 @@ + android:layout_height="match_parent" + android:visibility="gone" + tools:visibility="visible" /> + + + android:layout_height="match_parent" + android:visibility="gone" /> + +