From f9b5c673c587dc215b36b3abee44b9d0d9cc924b Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Sat, 25 Nov 2023 00:19:31 +0800 Subject: [PATCH] android, desktop: better handling of URI's (#3450) --- .../chat/simplex/common/platform/Modifier.android.kt | 3 ++- .../simplex/common/platform/VideoPlayer.android.kt | 2 +- .../kotlin/chat/simplex/common/model/ChatModel.kt | 3 +-- .../kotlin/chat/simplex/common/platform/Files.kt | 6 ++++++ .../kotlin/chat/simplex/common/platform/Modifier.kt | 3 ++- .../chat/simplex/common/platform/VideoPlayer.kt | 11 ++++------- .../chat/simplex/common/views/chat/ChatView.kt | 2 +- .../chat/simplex/common/platform/Files.desktop.kt | 4 ++-- .../chat/simplex/common/platform/Images.desktop.kt | 4 ++-- .../chat/simplex/common/platform/Modifier.desktop.kt | 8 ++++++-- .../simplex/common/platform/RecAndPlay.desktop.kt | 10 +++++----- .../chat/simplex/common/platform/Share.desktop.kt | 7 +++++-- .../simplex/common/platform/VideoPlayer.desktop.kt | 9 ++++----- .../chat/simplex/common/platform/Videos.desktop.kt | 2 +- .../simplex/common/views/helpers/Utils.desktop.kt | 12 +++++------- 15 files changed, 47 insertions(+), 39 deletions(-) diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Modifier.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Modifier.android.kt index 115027c1a..26ada2b7e 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Modifier.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Modifier.android.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.painter.Painter import com.google.accompanist.insets.navigationBarsWithImePadding +import java.io.File actual fun Modifier.navigationBarsWithImePadding(): Modifier = navigationBarsWithImePadding() @@ -19,7 +20,7 @@ actual fun ProvideWindowInsets( @Composable actual fun Modifier.desktopOnExternalDrag( enabled: Boolean, - onFiles: (List) -> Unit, + onFiles: (List) -> Unit, onImage: (Painter) -> Unit, onText: (String) -> Unit ): Modifier = this 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 1d62efbb4..d3b4609bc 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 @@ -71,7 +71,7 @@ actual class VideoPlayer actual constructor( private fun start(seek: Long? = null, onProgressUpdate: (position: Long?, state: TrackState) -> Unit): Boolean { val filepath = getAppFilePath(uri) if (filepath == null || !File(filepath).exists()) { - Log.e(TAG, "No such file: $uri") + Log.e(TAG, "No such file: $filepath") brokenVideo.value = true return false } 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 754e9dfe2..38c971a52 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 @@ -27,7 +27,6 @@ import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.* import java.io.File import java.net.URI -import java.net.URLDecoder import java.time.format.DateTimeFormatter import java.time.format.FormatStyle import java.util.* @@ -2363,7 +2362,7 @@ data class CryptoFile( companion object { fun plain(f: String): CryptoFile = CryptoFile(f, null) - fun desktopPlain(f: URI): CryptoFile = CryptoFile(URLDecoder.decode(f.rawPath, "UTF-8"), null) + fun desktopPlain(f: URI): CryptoFile = CryptoFile(f.toFile().absolutePath, null) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt index 877356e43..e80828865 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt @@ -7,6 +7,8 @@ import chat.simplex.common.views.helpers.generalGetString import chat.simplex.res.MR import java.io.* import java.net.URI +import java.net.URLDecoder +import java.net.URLEncoder expect val dataDir: File expect val tmpDir: File @@ -28,6 +30,10 @@ expect val remoteHostsDir: File expect fun desktopOpenDatabaseDir() +fun createURIFromPath(absolutePath: String): URI = URI.create(URLEncoder.encode(absolutePath, "UTF-8")) + +fun URI.toFile(): File = File(URLDecoder.decode(rawPath, "UTF-8").removePrefix("file:")) + fun copyFileToFile(from: File, to: URI, finally: () -> Unit) { try { to.outputStream().use { stream -> diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Modifier.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Modifier.kt index a143057a3..1141ab21a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Modifier.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Modifier.kt @@ -3,6 +3,7 @@ package chat.simplex.common.platform import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.painter.Painter +import java.io.File expect fun Modifier.navigationBarsWithImePadding(): Modifier @@ -16,7 +17,7 @@ expect fun ProvideWindowInsets( @Composable expect fun Modifier.desktopOnExternalDrag( enabled: Boolean = true, - onFiles: (List) -> Unit = {}, + onFiles: (List) -> Unit = {}, onImage: (Painter) -> Unit = {}, onText: (String) -> Unit = {} ): Modifier diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/VideoPlayer.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/VideoPlayer.kt index 5c3b50bbd..5e0f580d1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/VideoPlayer.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/VideoPlayer.kt @@ -43,16 +43,13 @@ object VideoPlayerHolder { ): VideoPlayer = players.getOrPut(uri to gallery) { VideoPlayer(uri, gallery, defaultPreview, defaultDuration, soundEnabled) } - fun enableSound(enable: Boolean, fileName: String?, gallery: Boolean): Boolean = - player(fileName, gallery)?.enableSound(enable) == true - - private fun player(fileName: String?, gallery: Boolean): VideoPlayer? { - fileName ?: return null - return players.values.firstOrNull { player -> player.uri.path?.endsWith(fileName) == true && player.gallery == gallery } + private fun player(uri: URI?, gallery: Boolean): VideoPlayer? { + uri ?: return null + return players.values.firstOrNull { player -> player.uri == uri && player.gallery == gallery } } fun release(uri: URI, gallery: Boolean, remove: Boolean) = - player(uri.path, gallery)?.release(remove).run { } + player(uri, gallery)?.release(remove).run { } fun stopAll() { players.values.forEach { it.stop() } 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 5975f674e..8eee43035 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 @@ -501,7 +501,7 @@ fun ChatLayout( .fillMaxWidth() .desktopOnExternalDrag( enabled = !attachmentDisabled.value && rememberUpdatedState(chat.userCanSend).value, - onFiles = { paths -> composeState.onFilesAttached(paths.map { URI.create(it) }) }, + onFiles = { paths -> composeState.onFilesAttached(paths.map { it.toURI() }) }, onImage = { // TODO: file is not saved anywhere?! val tmpFile = File.createTempFile("image", ".bmp", tmpDir) diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt index 0f7c13186..9b2368fcd 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt @@ -104,5 +104,5 @@ private fun fileFilterDescription(input: String): String = when(input) { else -> "" } -actual fun URI.inputStream(): InputStream? = File(URI("file:" + toString().removePrefix("file:"))).inputStream() -actual fun URI.outputStream(): OutputStream = File(URI("file:" + toString().removePrefix("file:"))).outputStream() +actual fun URI.inputStream(): InputStream? = toFile().inputStream() +actual fun URI.outputStream(): OutputStream = toFile().outputStream() diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt index 0df5ee815..0f7d140b0 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt @@ -157,7 +157,7 @@ actual fun ImageBitmap.scale(width: Int, height: Int): ImageBitmap { // LALAL actual fun isImage(uri: URI): Boolean { - val path = uri.path.lowercase() + val path = uri.toFile().path.lowercase() return path.endsWith(".gif") || path.endsWith(".webp") || path.endsWith(".png") || @@ -166,7 +166,7 @@ actual fun isImage(uri: URI): Boolean { } actual fun isAnimImage(uri: URI, drawable: Any?): Boolean { - val path = uri.path.lowercase() + val path = uri.toFile().path.lowercase() return path.endsWith(".gif") || path.endsWith(".webp") } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Modifier.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Modifier.desktop.kt index fa9f311d1..6f317acb9 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Modifier.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Modifier.desktop.kt @@ -4,6 +4,8 @@ import androidx.compose.foundation.contextMenuOpenDetector import androidx.compose.runtime.Composable import androidx.compose.ui.* import androidx.compose.ui.graphics.painter.Painter +import java.io.File +import java.net.URI actual fun Modifier.navigationBarsWithImePadding(): Modifier = this @@ -19,13 +21,15 @@ actual fun ProvideWindowInsets( @Composable actual fun Modifier.desktopOnExternalDrag( enabled: Boolean, - onFiles: (List) -> Unit, + onFiles: (List) -> Unit, onImage: (Painter) -> Unit, onText: (String) -> Unit ): Modifier = onExternalDrag(enabled) { when(val data = it.dragData) { - is DragData.FilesList -> onFiles(data.readFiles()) + // data.readFiles() returns filePath in URI format (where spaces replaces with %20). But it's an error-prone idea to work later + // with such format when everywhere we use absolutePath in File() format + is DragData.FilesList -> onFiles(data.readFiles().map { URI.create(it).toFile() }) is DragData.Image -> onImage(data.readImage()) is DragData.Text -> onText(data.readText()) } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt index 83351d772..b6d18aaf8 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt @@ -37,7 +37,7 @@ actual object AudioPlayer: AudioPlayerInterface { // Returns real duration of the track private fun start(fileSource: CryptoFile, seek: Int? = null, onProgressUpdate: (position: Int?, state: TrackState) -> Unit): Int? { - val absoluteFilePath = if (fileSource.isAbsolutePath) fileSource.filePath else getAppFilePath(fileSource.filePath) + val absoluteFilePath = if (fileSource.isAbsolutePath) fileSource.filePath else getAppFilePath(fileSource.filePath) if (!File(absoluteFilePath).exists()) { Log.e(TAG, "No such file: ${fileSource.filePath}") return null @@ -46,16 +46,16 @@ actual object AudioPlayer: AudioPlayerInterface { VideoPlayerHolder.stopAll() RecorderInterface.stopRecording?.invoke() val current = currentlyPlaying.value - if (current == null || current.first != fileSource) { + if (current == null || current.first != fileSource || !player.status().isPlayable) { stopListener() player.stop() runCatching { if (fileSource.cryptoArgs != null) { val tmpFile = fileSource.createTmpFileIfNeeded() decryptCryptoFile(absoluteFilePath, fileSource.cryptoArgs, tmpFile.absolutePath) - player.media().prepare(tmpFile.toURI().toString().replaceFirst("file:", "file://")) + player.media().prepare(tmpFile.absolutePath) } else { - player.media().prepare(File(absoluteFilePath).toURI().toString().replaceFirst("file:", "file://")) + player.media().prepare(absoluteFilePath) } }.onFailure { Log.e(TAG, it.stackTraceToString()) @@ -171,7 +171,7 @@ actual object AudioPlayer: AudioPlayerInterface { var res: Int? = null try { val helperPlayer = AudioPlayerComponent().mediaPlayer() - helperPlayer.media().startPaused(File(unencryptedFilePath).toURI().toString().replaceFirst("file:", "file://")) + helperPlayer.media().startPaused(unencryptedFilePath) res = helperPlayer.duration helperPlayer.stop() helperPlayer.release() diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Share.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Share.desktop.kt index f7728f9c6..b40b892de 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Share.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Share.desktop.kt @@ -4,6 +4,7 @@ import androidx.compose.ui.platform.ClipboardManager import androidx.compose.ui.platform.UriHandler import androidx.compose.ui.text.AnnotatedString import chat.simplex.common.model.* +import chat.simplex.common.views.helpers.generalGetString import chat.simplex.common.views.helpers.withApi import java.io.File import java.net.URI @@ -25,14 +26,16 @@ actual fun shareFile(text: String, fileSource: CryptoFile) { withApi { FileChooserLauncher(false) { to: URI? -> if (to != null) { + val absolutePath = if (fileSource.isAbsolutePath) fileSource.filePath else getAppFilePath(fileSource.filePath) if (fileSource.cryptoArgs != null) { try { - decryptCryptoFile(getAppFilePath(fileSource.filePath), fileSource.cryptoArgs, to.path) + decryptCryptoFile(absolutePath, fileSource.cryptoArgs, to.toFile().absolutePath) + showToast(generalGetString(MR.strings.file_saved)) } catch (e: Exception) { Log.e(TAG, "Unable to decrypt crypto file: " + e.stackTraceToString()) } } else { - copyFileToFile(File(fileSource.filePath), to) {} + copyFileToFile(File(absolutePath), to) {} } } }.launch(fileSource.filePath) 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 fa0a8247a..ec487bc88 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 @@ -52,7 +52,7 @@ actual class VideoPlayer actual constructor( private fun start(seek: Long? = null, onProgressUpdate: (position: Long?, state: TrackState) -> Unit): Boolean { val filepath = getAppFilePath(uri) if (filepath == null || !File(filepath).exists()) { - Log.e(TAG, "No such file: $uri") + Log.e(TAG, "No such file: $filepath") brokenVideo.value = true return false } @@ -62,10 +62,9 @@ actual class VideoPlayer actual constructor( } AudioPlayer.stop() VideoPlayerHolder.stopAll() - val playerFilePath = uri.toString().replaceFirst("file:", "file://") if (listener.value == null) { runCatching { - player.media().prepare(playerFilePath) + player.media().prepare(uri.toFile().absolutePath) if (seek != null) { player.seekTo(seek.toInt()) } @@ -217,12 +216,12 @@ actual class VideoPlayer actual constructor( suspend fun getBitmapFromVideo(defaultPreview: ImageBitmap?, uri: URI?, withAlertOnException: Boolean = true): VideoPlayerInterface.PreviewAndDuration = withContext(playerThread.asCoroutineDispatcher()) { val mediaComponent = getOrCreateHelperPlayer() val player = mediaComponent.mediaPlayer() - if (uri == null || !File(uri.rawPath).exists()) { + if (uri == null || !uri.toFile().exists()) { if (withAlertOnException) showVideoDecodingException() return@withContext VideoPlayerInterface.PreviewAndDuration(preview = defaultPreview, timestamp = 0L, duration = 0L) } - player.media().startPaused(uri.toString().replaceFirst("file:", "file://")) + player.media().startPaused(uri.toFile().absolutePath) val start = System.currentTimeMillis() var snap: BufferedImage? = null while (snap == null && start + 5000 > System.currentTimeMillis()) { diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Videos.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Videos.desktop.kt index 54a511e08..e9924914e 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Videos.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Videos.desktop.kt @@ -3,7 +3,7 @@ package chat.simplex.common.platform import java.net.URI fun isVideo(uri: URI): Boolean { - val path = uri.path.lowercase() + val path = uri.toFile().path.lowercase() return path.endsWith(".mov") || path.endsWith(".avi") || path.endsWith(".mp4") || diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt index 9413cbb40..eb1792474 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt @@ -8,14 +8,12 @@ import androidx.compose.ui.unit.Density import chat.simplex.common.model.* import chat.simplex.common.platform.* import chat.simplex.common.simplexWindowState -import chat.simplex.res.MR import java.io.ByteArrayInputStream import java.io.File import java.net.URI import javax.imageio.ImageIO import kotlin.io.encoding.Base64 import kotlin.io.encoding.ExperimentalEncodingApi -import kotlin.io.path.toPath private val bStyle = SpanStyle(fontWeight = FontWeight.Bold) private val iStyle = SpanStyle(fontStyle = FontStyle.Italic) @@ -90,9 +88,9 @@ actual fun escapedHtmlToAnnotatedString(text: String, density: Density): Annotat actual fun getAppFileUri(fileName: String): URI { val rh = chatModel.currentRemoteHost.value return if (rh == null) { - URI(appFilesDir.toURI().toString() + "/" + fileName) + createURIFromPath(appFilesDir.absolutePath + "/" + fileName) } else { - URI(dataDir.absolutePath + "/remote_hosts/" + rh.storePath + "/simplex_v1_files/" + fileName) + createURIFromPath(dataDir.absolutePath + "/remote_hosts/" + rh.storePath + "/simplex_v1_files/" + fileName) } } @@ -116,11 +114,11 @@ actual suspend fun getLoadedImage(file: CIFile?): Pair? } } -actual fun getFileName(uri: URI): String? = uri.toPath().toFile().name +actual fun getFileName(uri: URI): String? = uri.toFile().name -actual fun getAppFilePath(uri: URI): String? = uri.path +actual fun getAppFilePath(uri: URI): String? = uri.toFile().absolutePath -actual fun getFileSize(uri: URI): Long? = uri.toPath().toFile().length() +actual fun getFileSize(uri: URI): Long? = uri.toFile().length() actual fun getBitmapFromUri(uri: URI, withAlertOnException: Boolean): ImageBitmap? = try {