From be0c791c4361339c8a39b6e441dbdc26967ab0d2 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Mon, 15 Jan 2024 22:00:57 +0700 Subject: [PATCH] android, desktop: local video encryption (#3678) * android, desktop: local video encryption * refactor * different look of progress indicator --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../common/platform/VideoPlayer.android.kt | 8 +- .../chat/simplex/common/model/ChatModel.kt | 30 ++++- .../chat/simplex/common/model/SimpleXAPI.kt | 6 +- .../simplex/common/views/chat/ChatView.kt | 16 +-- .../simplex/common/views/chat/ComposeView.kt | 9 +- .../common/views/chat/item/CIFileView.kt | 5 +- .../common/views/chat/item/CIImageView.kt | 5 +- .../common/views/chat/item/CIVIdeoView.kt | 117 ++++++++++++++++-- .../common/views/chat/item/CIVoiceView.kt | 8 +- .../common/views/chat/item/ChatItemView.kt | 6 +- .../common/views/chat/item/FramedItemView.kt | 4 +- .../views/chat/item/ImageFullScreenView.kt | 26 +++- .../simplex/common/views/helpers/Utils.kt | 13 +- .../common/platform/VideoPlayer.desktop.kt | 2 +- 14 files changed, 194 insertions(+), 61 deletions(-) diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/VideoPlayer.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/VideoPlayer.android.kt index d3b4609bc..6ed054cae 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/VideoPlayer.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/VideoPlayer.android.kt @@ -33,9 +33,9 @@ actual class VideoPlayer actual constructor( override val duration: MutableState = mutableStateOf(defaultDuration) override val preview: MutableState = mutableStateOf(defaultPreview) - init { - setPreviewAndDuration() - } + + // Currently unused because we use low-quality preview + // init { setPreviewAndDuration() } val player = ExoPlayer.Builder(androidAppContext, DefaultRenderersFactory(androidAppContext)) @@ -69,7 +69,7 @@ actual class VideoPlayer actual constructor( } private fun start(seek: Long? = null, onProgressUpdate: (position: Long?, state: TrackState) -> Unit): Boolean { - val filepath = getAppFilePath(uri) + val filepath = if (uri.scheme == "file") uri.toFile().absolutePath else getAppFilePath(uri) if (filepath == null || !File(filepath).exists()) { Log.e(TAG, "No such file: $filepath") brokenVideo.value = true diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index e96a385df..5ec9d1f1e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -1644,10 +1644,6 @@ data class ChatItem ( val encryptedFile: Boolean? = if (file?.fileSource == null) null else file.fileSource.cryptoArgs != null - val encryptLocalFile: Boolean - get() = content.msgContent !is MsgContent.MCVideo && - chatController.appPrefs.privacyEncryptLocalFiles.get() - val memberDisplayName: String? get() = if (chatDir is CIDirection.GroupRcv) chatDir.groupMember.chatViewName else null @@ -2432,10 +2428,36 @@ data class CryptoFile( tmpFile?.delete() } + private fun decryptToTmpFile(): URI? { + val absoluteFilePath = if (isAbsolutePath) filePath else getAppFilePath(filePath) + val tmpFile = createTmpFileIfNeeded() + decryptCryptoFile(absoluteFilePath, cryptoArgs ?: return null, tmpFile.absolutePath) + return tmpFile.toURI() + } + + fun decryptedGet(): URI? { + val decrypted = decryptedUris[filePath] + return if (decrypted != null && decrypted.toFile().exists()) { + decrypted + } else { + null + } + } + + fun decryptedGetOrCreate(): URI? { + val decrypted = decryptedGet() ?: decryptToTmpFile() + if (decrypted != null) { + decryptedUris[filePath] = decrypted + } + return decrypted + } + companion object { fun plain(f: String): CryptoFile = CryptoFile(f, null) fun desktopPlain(f: URI): CryptoFile = CryptoFile(f.toFile().absolutePath, null) + + private val decryptedUris = mutableMapOf() } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 96437ad33..97853e2fd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -1667,8 +1667,7 @@ object ChatController { ((mc is MsgContent.MCImage && file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV) || (mc is MsgContent.MCVideo && file.fileSize <= MAX_VIDEO_SIZE_AUTO_RCV) || (mc is MsgContent.MCVoice && file.fileSize <= MAX_VOICE_SIZE_AUTO_RCV && file.fileStatus !is CIFileStatus.RcvAccepted))) { - withBGApi { receiveFile(rhId, r.user, file.fileId, encrypted = cItem.encryptLocalFile && chatController.appPrefs - .privacyEncryptLocalFiles.get(), auto = true) } + withBGApi { receiveFile(rhId, r.user, file.fileId, auto = true) } } if (cItem.showNotification && (allowedToShowNotification() || chatModel.chatId.value != cInfo.id || chatModel.remoteHostId() != rhId)) { ntfManager.notifyMessageReceived(r.user, cInfo, cItem) @@ -2032,7 +2031,8 @@ object ChatController { } } - suspend fun receiveFile(rhId: Long?, user: UserLike, fileId: Long, encrypted: Boolean, auto: Boolean = false) { + suspend fun receiveFile(rhId: Long?, user: UserLike, fileId: Long, auto: Boolean = false) { + val encrypted = appPrefs.privacyEncryptLocalFiles.get() val chatItem = apiReceiveFile(rhId, fileId, encrypted = encrypted, auto = auto) if (chatItem != null) { chatItemSimpleUpdate(rhId, user, chatItem) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index b197c4ada..8a6c74bb3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -290,8 +290,8 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: } } }, - receiveFile = { fileId, encrypted -> - withBGApi { chatModel.controller.receiveFile(chatRh, user, fileId, encrypted) } + receiveFile = { fileId -> + withBGApi { chatModel.controller.receiveFile(chatRh, user, fileId) } }, cancelFile = { fileId -> withBGApi { chatModel.controller.cancelFile(chatRh, user, fileId) } @@ -505,7 +505,7 @@ fun ChatLayout( loadPrevMessages: () -> Unit, deleteMessage: (Long, CIDeleteMode) -> Unit, deleteMessages: (List) -> Unit, - receiveFile: (Long, Boolean) -> Unit, + receiveFile: (Long) -> Unit, cancelFile: (Long) -> Unit, joinGroup: (Long, () -> Unit) -> Unit, startCall: (CallMediaType) -> Unit, @@ -830,7 +830,7 @@ fun BoxWithConstraintsScope.ChatItemsList( loadPrevMessages: () -> Unit, deleteMessage: (Long, CIDeleteMode) -> Unit, deleteMessages: (List) -> Unit, - receiveFile: (Long, Boolean) -> Unit, + receiveFile: (Long) -> Unit, cancelFile: (Long) -> Unit, joinGroup: (Long, () -> Unit) -> Unit, acceptCall: (Contact) -> Unit, @@ -1344,7 +1344,7 @@ fun chatViewItemsRange(currIndex: Int?, prevHidden: Int?): IntRange? = sealed class ProviderMedia { data class Image(val data: ByteArray, val image: ImageBitmap): ProviderMedia() - data class Video(val uri: URI, val preview: String): ProviderMedia() + data class Video(val uri: URI, val fileSource: CryptoFile?, val preview: String): ProviderMedia() } private fun providerForGallery( @@ -1394,7 +1394,7 @@ private fun providerForGallery( val filePath = if (chatModel.connectedToRemote() && item.file?.loaded == true) getAppFilePath(item.file.fileName) else getLoadedFilePath(item.file) if (filePath != null) { val uri = getAppFileUri(filePath.substringAfterLast(File.separator)) - ProviderMedia.Video(uri, (item.content.msgContent as MsgContent.MCVideo).image) + ProviderMedia.Video(uri, item.file?.fileSource, (item.content.msgContent as MsgContent.MCVideo).image) } else null } else -> null @@ -1487,7 +1487,7 @@ fun PreviewChatLayout() { loadPrevMessages = {}, deleteMessage = { _, _ -> }, deleteMessages = { _ -> }, - receiveFile = { _, _ -> }, + receiveFile = { _ -> }, cancelFile = {}, joinGroup = { _, _ -> }, startCall = {}, @@ -1560,7 +1560,7 @@ fun PreviewGroupChatLayout() { loadPrevMessages = {}, deleteMessage = { _, _ -> }, deleteMessages = {}, - receiveFile = { _, _ -> }, + receiveFile = { _ -> }, cancelFile = {}, joinGroup = { _, _ -> }, startCall = {}, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index 30786fae9..e46e7a305 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -459,16 +459,15 @@ fun ComposeView( is ComposePreview.CLinkPreview -> msgs.add(checkLinkPreview()) is ComposePreview.MediaPreview -> { preview.content.forEachIndexed { index, it -> - val encrypted = chatController.appPrefs.privacyEncryptLocalFiles.get() val file = when (it) { is UploadContent.SimpleImage -> - if (remoteHost == null) saveImage(it.uri, encrypted = encrypted) + if (remoteHost == null) saveImage(it.uri) else desktopSaveImageInTmp(it.uri) is UploadContent.AnimatedImage -> - if (remoteHost == null) saveAnimImage(it.uri, encrypted = encrypted) + if (remoteHost == null) saveAnimImage(it.uri) else CryptoFile.desktopPlain(it.uri) is UploadContent.Video -> - if (remoteHost == null) saveFileFromUri(it.uri, encrypted = false) + if (remoteHost == null) saveFileFromUri(it.uri) else CryptoFile.desktopPlain(it.uri) } if (file != null) { @@ -506,7 +505,7 @@ fun ComposeView( } is ComposePreview.FilePreview -> { val file = if (remoteHost == null) { - saveFileFromUri(preview.uri, encrypted = chatController.appPrefs.privacyEncryptLocalFiles.get()) + saveFileFromUri(preview.uri) } else { CryptoFile.desktopPlain(preview.uri) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt index 152d7edb7..79af76da8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt @@ -28,7 +28,7 @@ import java.net.URI fun CIFileView( file: CIFile?, edited: Boolean, - receiveFile: (Long, Boolean) -> Unit + receiveFile: (Long) -> Unit ) { val saveFileLauncher = rememberSaveFileLauncher(ciFile = file) @@ -71,8 +71,7 @@ fun CIFileView( when (file.fileStatus) { is CIFileStatus.RcvInvitation -> { if (fileSizeValid()) { - val encrypted = chatController.appPrefs.privacyEncryptLocalFiles.get() - receiveFile(file.fileId, encrypted) + receiveFile(file.fileId) } else { AlertManager.shared.showAlertMsg( generalGetString(MR.strings.large_file), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt index 1e5919c0b..cbec5c289 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt @@ -32,11 +32,10 @@ import java.net.URI fun CIImageView( image: String, file: CIFile?, - encryptLocalFile: Boolean, metaColor: Color, imageProvider: () -> ImageGalleryProvider, showMenu: MutableState, - receiveFile: (Long, Boolean) -> Unit + receiveFile: (Long) -> Unit ) { @Composable fun progressIndicator() { @@ -181,7 +180,7 @@ fun CIImageView( when (file.fileStatus) { CIFileStatus.RcvInvitation -> if (fileSizeValid()) { - receiveFile(file.fileId, encryptLocalFile) + receiveFile(file.fileId) } else { AlertManager.shared.showAlertMsg( generalGetString(MR.strings.large_file), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVIdeoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVIdeoView.kt index 42e90c35f..609a895e4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVIdeoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVIdeoView.kt @@ -21,7 +21,6 @@ import chat.simplex.common.views.helpers.* import chat.simplex.common.model.* import chat.simplex.common.platform.* import dev.icerock.moko.resources.StringResource -import kotlinx.coroutines.flow.* import java.io.File import java.net.URI @@ -32,7 +31,7 @@ fun CIVideoView( file: CIFile?, imageProvider: () -> ImageGalleryProvider, showMenu: MutableState, - receiveFile: (Long, Boolean) -> Unit + receiveFile: (Long) -> Unit ) { Box( Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID), @@ -52,21 +51,30 @@ fun CIVideoView( } val f = filePath.value if (file != null && f != null) { - val uri = remember(filePath) { getAppFileUri(f.substringAfterLast(File.separator)) } val view = LocalMultiplatformView() - VideoView(uri, file, preview, duration * 1000L, showMenu, onClick = { + val openFullscreen = { hideKeyboard(view) ModalManager.fullscreen.showCustomModal(animated = false) { close -> ImageFullScreenView(imageProvider, close) } - }) + } + + val uri = remember(filePath) { getAppFileUri(f.substringAfterLast(File.separator)) } + val autoPlay = remember { mutableStateOf(false) } + val uriDecrypted = remember(filePath) { mutableStateOf(if (file.fileSource?.cryptoArgs == null) uri else file.fileSource.decryptedGet()) } + val decrypted = uriDecrypted.value + if (decrypted != null) { + VideoView(decrypted, file, preview, duration * 1000L, autoPlay, showMenu, openFullscreen = openFullscreen) + } else { + VideoViewEncrypted(uriDecrypted, file, preview, duration * 1000L, autoPlay, showMenu, openFullscreen = openFullscreen) + } } else { Box { VideoPreviewImageView(preview, onClick = { if (file != null) { when (file.fileStatus) { CIFileStatus.RcvInvitation -> - receiveFileIfValidSize(file, encrypted = false, receiveFile) + receiveFileIfValidSize(file, receiveFile) CIFileStatus.RcvAccepted -> when (file.fileProtocol) { FileProtocol.XFTP -> @@ -95,7 +103,7 @@ fun CIVideoView( DurationProgress(file, remember { mutableStateOf(false) }, remember { mutableStateOf(duration * 1000L) }, remember { mutableStateOf(0L) }/*, soundEnabled*/) } if (file?.fileStatus is CIFileStatus.RcvInvitation) { - PlayButton(error = false, { showMenu.value = true }) { receiveFileIfValidSize(file, encrypted = false, receiveFile) } + PlayButton(error = false, { showMenu.value = true }) { receiveFileIfValidSize(file, receiveFile) } } } } @@ -104,7 +112,40 @@ fun CIVideoView( } @Composable -private fun VideoView(uri: URI, file: CIFile, defaultPreview: ImageBitmap, defaultDuration: Long, showMenu: MutableState, onClick: () -> Unit) { +private fun VideoViewEncrypted( + uriUnencrypted: MutableState, + file: CIFile, + defaultPreview: ImageBitmap, + defaultDuration: Long, + autoPlay: MutableState, + showMenu: MutableState, + openFullscreen: () -> Unit, +) { + var decryptionInProgress by rememberSaveable(file.fileName) { mutableStateOf(false) } + val onLongClick = { showMenu.value = true } + Box { + VideoPreviewImageView(defaultPreview, if (decryptionInProgress) {{}} else openFullscreen, onLongClick) + if (decryptionInProgress) { + VideoDecryptionProgress(onLongClick = onLongClick) + } else { + PlayButton(false, onLongClick = onLongClick) { + decryptionInProgress = true + withBGApi { + try { + uriUnencrypted.value = file.fileSource?.decryptedGetOrCreate() + autoPlay.value = uriUnencrypted.value != null + } finally { + decryptionInProgress = false + } + } + } + } + DurationProgress(file, remember { mutableStateOf(false) }, remember { mutableStateOf(defaultDuration) }, remember { mutableStateOf(0L) }) + } +} + +@Composable +private fun VideoView(uri: URI, file: CIFile, defaultPreview: ImageBitmap, defaultDuration: Long, autoPlay: MutableState, showMenu: MutableState, openFullscreen: () -> Unit) { val player = remember(uri) { VideoPlayerHolder.getOrCreate(uri, false, defaultPreview, defaultDuration, true) } val videoPlaying = remember(uri.path) { player.videoPlaying } val progress = remember(uri.path) { player.progress } @@ -121,6 +162,13 @@ private fun VideoView(uri: URI, file: CIFile, defaultPreview: ImageBitmap, defau player.stop() } val showPreview = remember { derivedStateOf { !videoPlaying.value || progress.value == 0L } } + LaunchedEffect(uri) { + if (autoPlay.value) play() + } + // Drop autoPlay only when show preview changes to prevent blinking of the view + KeyChangeEffect(showPreview.value) { + autoPlay.value = false + } DisposableEffect(Unit) { onDispose { stop() @@ -133,13 +181,15 @@ private fun VideoView(uri: URI, file: CIFile, defaultPreview: ImageBitmap, defau PlayerView( player, width, - onClick = onClick, + onClick = openFullscreen, onLongClick = onLongClick, stop ) if (showPreview.value) { - VideoPreviewImageView(preview, onClick, onLongClick) - PlayButton(brokenVideo, onLongClick = onLongClick, play) + VideoPreviewImageView(preview, openFullscreen, onLongClick) + if (!autoPlay.value) { + PlayButton(brokenVideo, onLongClick = onLongClick, play) + } } DurationProgress(file, videoPlaying, duration, progress/*, soundEnabled*/) } @@ -172,6 +222,31 @@ private fun BoxScope.PlayButton(error: Boolean = false, onLongClick: () -> Unit, } } +@Composable +fun BoxScope.VideoDecryptionProgress(onLongClick: () -> Unit) { + Surface( + Modifier.align(Alignment.Center), + color = Color.Black.copy(alpha = 0.25f), + shape = RoundedCornerShape(percent = 50), + contentColor = LocalContentColor.current + ) { + Box( + Modifier + .defaultMinSize(minWidth = 40.dp, minHeight = 40.dp) + .combinedClickable(onClick = {}, onLongClick = onLongClick) + .onRightClick { onLongClick.invoke() }, + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + Modifier + .size(30.dp), + color = Color.White, + strokeWidth = 2.5.dp + ) + } + } +} + @Composable private fun DurationProgress(file: CIFile, playing: MutableState, duration: MutableState, progress: MutableState/*, soundEnabled: MutableState*/) { if (duration.value > 0L || progress.value > 0) { @@ -235,6 +310,22 @@ fun VideoPreviewImageView(preview: ImageBitmap, onClick: () -> Unit, onLongClick ) } +@Composable +fun VideoPreviewImageViewFullScreen(preview: ImageBitmap, onClick: () -> Unit, onLongClick: () -> Unit) { + Image( + preview, + contentDescription = stringResource(MR.strings.video_descr), + modifier = Modifier + .fillMaxSize() + .combinedClickable( + onLongClick = onLongClick, + onClick = onClick + ) + .onRightClick(onLongClick), + contentScale = ContentScale.FillWidth, + ) +} + @Composable expect fun LocalWindowWidth(): Dp @@ -319,9 +410,9 @@ private fun fileSizeValid(file: CIFile?): Boolean { return false } -private fun receiveFileIfValidSize(file: CIFile, encrypted: Boolean, receiveFile: (Long, Boolean) -> Unit) { +private fun receiveFileIfValidSize(file: CIFile, receiveFile: (Long) -> Unit) { if (fileSizeValid(file)) { - receiveFile(file.fileId, encrypted) + receiveFile(file.fileId) } else { AlertManager.shared.showAlertMsg( generalGetString(MR.strings.large_file), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt index 8b01d9d1b..e59c5f137 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt @@ -36,7 +36,7 @@ fun CIVoiceView( ci: ChatItem, timedMessagesTTL: Int?, longClick: () -> Unit, - receiveFile: (Long, Boolean) -> Unit, + receiveFile: (Long) -> Unit, ) { Row( Modifier.padding(top = if (hasText) 14.dp else 4.dp, bottom = if (hasText) 14.dp else 6.dp, start = if (hasText) 6.dp else 0.dp, end = if (hasText) 6.dp else 0.dp), @@ -105,7 +105,7 @@ private fun VoiceLayout( play: () -> Unit, pause: () -> Unit, longClick: () -> Unit, - receiveFile: (Long, Boolean) -> Unit, + receiveFile: (Long) -> Unit, onProgressChanged: (Int) -> Unit, ) { @Composable @@ -260,7 +260,7 @@ private fun VoiceMsgIndicator( play: () -> Unit, pause: () -> Unit, longClick: () -> Unit, - receiveFile: (Long, Boolean) -> Unit, + receiveFile: (Long) -> Unit, ) { val strokeWidth = with(LocalDensity.current) { 3.dp.toPx() } val strokeColor = MaterialTheme.colors.primary @@ -280,7 +280,7 @@ private fun VoiceMsgIndicator( } } else { if (file?.fileStatus is CIFileStatus.RcvInvitation) { - PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, { receiveFile(file.fileId, chatController.appPrefs.privacyEncryptLocalFiles.get()) }, {}, longClick = longClick) + PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, { receiveFile(file.fileId) }, {}, longClick = longClick) } else if (file?.fileStatus is CIFileStatus.RcvTransfer || file?.fileStatus is CIFileStatus.RcvAccepted ) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index 38507237a..6a1aeb21c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -50,7 +50,7 @@ fun ChatItemView( range: IntRange?, deleteMessage: (Long, CIDeleteMode) -> Unit, deleteMessages: (List) -> Unit, - receiveFile: (Long, Boolean) -> Unit, + receiveFile: (Long) -> Unit, cancelFile: (Long) -> Unit, joinGroup: (Long, () -> Unit) -> Unit, acceptCall: (Contact) -> Unit, @@ -746,7 +746,7 @@ fun PreviewChatItemView() { range = 0..1, deleteMessage = { _, _ -> }, deleteMessages = { _ -> }, - receiveFile = { _, _ -> }, + receiveFile = { _ -> }, cancelFile = {}, joinGroup = { _, _ -> }, acceptCall = { _ -> }, @@ -780,7 +780,7 @@ fun PreviewChatItemViewDeletedContent() { range = 0..1, deleteMessage = { _, _ -> }, deleteMessages = { _ -> }, - receiveFile = { _, _ -> }, + receiveFile = { _ -> }, cancelFile = {}, joinGroup = { _, _ -> }, acceptCall = { _ -> }, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt index 475e9779e..83ec134d8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt @@ -35,7 +35,7 @@ fun FramedItemView( imageProvider: (() -> ImageGalleryProvider)? = null, linkMode: SimplexLinkMode, showMenu: MutableState, - receiveFile: (Long, Boolean) -> Unit, + receiveFile: (Long) -> Unit, onLinkLongClick: (link: String) -> Unit = {}, scrollToItem: (Long) -> Unit = {}, ) { @@ -232,7 +232,7 @@ fun FramedItemView( } else { when (val mc = ci.content.msgContent) { is MsgContent.MCImage -> { - CIImageView(image = mc.image, file = ci.file, ci.encryptLocalFile, metaColor = metaColor, imageProvider ?: return@PriorityLayout, showMenu, receiveFile) + CIImageView(image = mc.image, file = ci.file, metaColor = metaColor, imageProvider ?: return@PriorityLayout, showMenu, receiveFile) if (mc.text == "" && !ci.meta.isLive) { metaColor = Color.White } else { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt index 825211ac8..16d59f6ae 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.* import androidx.compose.ui.input.pointer.* import androidx.compose.ui.layout.onGloballyPositioned +import chat.simplex.common.model.CryptoFile import chat.simplex.common.platform.* import chat.simplex.common.views.chat.ProviderMedia import chat.simplex.common.views.helpers.* @@ -136,9 +137,15 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () -> FullScreenImageView(modifier, data, imageBitmap) } else if (media is ProviderMedia.Video) { val preview = remember(media.uri.path) { base64ToBitmap(media.preview) } - VideoView(modifier, media.uri, preview, index == settledCurrentPage, close) - DisposableEffect(Unit) { - onDispose { playersToRelease.add(media.uri) } + val uriDecrypted = remember(media.uri.path) { mutableStateOf(if (media.fileSource?.cryptoArgs == null) media.uri else media.fileSource.decryptedGet()) } + val decrypted = uriDecrypted.value + if (decrypted != null) { + VideoView(modifier, decrypted, preview, index == settledCurrentPage, close) + DisposableEffect(Unit) { + onDispose { playersToRelease.add(decrypted) } + } + } else if (media.fileSource != null) { + VideoViewEncrypted(uriDecrypted, media.fileSource, preview) } } } @@ -154,6 +161,19 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () -> @Composable expect fun FullScreenImageView(modifier: Modifier, data: ByteArray, imageBitmap: ImageBitmap) +@Composable +private fun VideoViewEncrypted(uriUnencrypted: MutableState, fileSource: CryptoFile, defaultPreview: ImageBitmap) { + LaunchedEffect(Unit) { + withBGApi { + uriUnencrypted.value = fileSource.decryptedGetOrCreate() + } + } + Box(contentAlignment = Alignment.Center) { + VideoPreviewImageViewFullScreen(defaultPreview, {}, {}) + VideoDecryptionProgress {} + } +} + @Composable private fun VideoView(modifier: Modifier, uri: URI, defaultPreview: ImageBitmap, currentPage: Boolean, close: () -> Unit) { val player = remember(uri) { VideoPlayerHolder.getOrCreate(uri, true, defaultPreview, 0L, true) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt index 91ee1b5da..a067cb2dd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt @@ -165,13 +165,14 @@ fun getThemeFromUri(uri: URI, withAlertOnException: Boolean = true): ThemeOverri return null } -fun saveImage(uri: URI, encrypted: Boolean): CryptoFile? { +fun saveImage(uri: URI): CryptoFile? { val bitmap = getBitmapFromUri(uri) ?: return null - return saveImage(bitmap, encrypted) + return saveImage(bitmap) } -fun saveImage(image: ImageBitmap, encrypted: Boolean): CryptoFile? { +fun saveImage(image: ImageBitmap): CryptoFile? { return try { + val encrypted = chatController.appPrefs.privacyEncryptLocalFiles.get() val ext = if (image.hasAlpha()) "png" else "jpg" val dataResized = resizeImageToDataSize(image, ext == "png", maxDataSize = MAX_IMAGE_SIZE) val destFileName = generateNewFileName("IMG", ext, File(getAppFilePath(""))) @@ -210,8 +211,9 @@ fun desktopSaveImageInTmp(uri: URI): CryptoFile? { } } -fun saveAnimImage(uri: URI, encrypted: Boolean): CryptoFile? { +fun saveAnimImage(uri: URI): CryptoFile? { return try { + val encrypted = chatController.appPrefs.privacyEncryptLocalFiles.get() val filename = getFileName(uri)?.lowercase() var ext = when { // remove everything but extension @@ -237,8 +239,9 @@ fun saveAnimImage(uri: URI, encrypted: Boolean): CryptoFile? { expect suspend fun saveTempImageUncompressed(image: ImageBitmap, asPng: Boolean): File? -fun saveFileFromUri(uri: URI, encrypted: Boolean, withAlertOnException: Boolean = true): CryptoFile? { +fun saveFileFromUri(uri: URI, withAlertOnException: Boolean = true): CryptoFile? { return try { + val encrypted = chatController.appPrefs.privacyEncryptLocalFiles.get() val inputStream = uri.inputStream() val fileToSave = getFileName(uri) return if (inputStream != null && fileToSave != null) { diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/VideoPlayer.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/VideoPlayer.desktop.kt index ec487bc88..dcae78466 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/VideoPlayer.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/VideoPlayer.desktop.kt @@ -29,7 +29,7 @@ actual class VideoPlayer actual constructor( override val brokenVideo: MutableState = mutableStateOf(false) override val videoPlaying: MutableState = mutableStateOf(false) override val progress: MutableState = mutableStateOf(0L) - override val duration: MutableState = mutableStateOf(0L) + override val duration: MutableState = mutableStateOf(defaultDuration) override val preview: MutableState = mutableStateOf(defaultPreview) val mediaPlayerComponent by lazy { runBlocking(playerThread.asCoroutineDispatcher()) { getOrCreatePlayer() } }