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>
This commit is contained in:
Stanislav Dmitrenko 2024-01-15 22:00:57 +07:00 committed by GitHub
parent 46fe0fc671
commit be0c791c43
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 194 additions and 61 deletions

View File

@ -33,9 +33,9 @@ actual class VideoPlayer actual constructor(
override val duration: MutableState<Long> = mutableStateOf(defaultDuration)
override val preview: MutableState<ImageBitmap> = 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

View File

@ -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<String, URI>()
}
}

View File

@ -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)

View File

@ -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<Long>) -> 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<Long>) -> 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 = {},

View File

@ -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)
}

View File

@ -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),

View File

@ -32,11 +32,10 @@ import java.net.URI
fun CIImageView(
image: String,
file: CIFile?,
encryptLocalFile: Boolean,
metaColor: Color,
imageProvider: () -> ImageGalleryProvider,
showMenu: MutableState<Boolean>,
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),

View File

@ -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<Boolean>,
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<Boolean>, onClick: () -> Unit) {
private fun VideoViewEncrypted(
uriUnencrypted: MutableState<URI?>,
file: CIFile,
defaultPreview: ImageBitmap,
defaultDuration: Long,
autoPlay: MutableState<Boolean>,
showMenu: MutableState<Boolean>,
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<Boolean>, showMenu: MutableState<Boolean>, 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<Boolean>, duration: MutableState<Long>, progress: MutableState<Long>/*, soundEnabled: MutableState<Boolean>*/) {
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),

View File

@ -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
) {

View File

@ -50,7 +50,7 @@ fun ChatItemView(
range: IntRange?,
deleteMessage: (Long, CIDeleteMode) -> Unit,
deleteMessages: (List<Long>) -> 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 = { _ -> },

View File

@ -35,7 +35,7 @@ fun FramedItemView(
imageProvider: (() -> ImageGalleryProvider)? = null,
linkMode: SimplexLinkMode,
showMenu: MutableState<Boolean>,
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 {

View File

@ -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<URI?>, 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) }

View File

@ -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) {

View File

@ -29,7 +29,7 @@ actual class VideoPlayer actual constructor(
override val brokenVideo: MutableState<Boolean> = mutableStateOf(false)
override val videoPlaying: MutableState<Boolean> = mutableStateOf(false)
override val progress: MutableState<Long> = mutableStateOf(0L)
override val duration: MutableState<Long> = mutableStateOf(0L)
override val duration: MutableState<Long> = mutableStateOf(defaultDuration)
override val preview: MutableState<ImageBitmap> = mutableStateOf(defaultPreview)
val mediaPlayerComponent by lazy { runBlocking(playerThread.asCoroutineDispatcher()) { getOrCreatePlayer() } }