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:
parent
46fe0fc671
commit
be0c791c43
@ -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
|
||||
|
@ -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>()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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 = {},
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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),
|
||||
|
@ -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,14 +181,16 @@ 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)
|
||||
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),
|
||||
|
@ -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
|
||||
) {
|
||||
|
@ -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 = { _ -> },
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
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(media.uri) }
|
||||
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) }
|
||||
|
@ -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) {
|
||||
|
@ -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() } }
|
||||
|
Loading…
Reference in New Issue
Block a user