android, desktop: better handling of URI's (#3450)

This commit is contained in:
Stanislav Dmitrenko 2023-11-25 00:19:31 +08:00 committed by GitHub
parent 8ce9dd7ab6
commit f9b5c673c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 47 additions and 39 deletions

View File

@ -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<String>) -> Unit,
onFiles: (List<File>) -> Unit,
onImage: (Painter) -> Unit,
onText: (String) -> Unit
): Modifier = this

View File

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

View File

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

View File

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

View File

@ -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<String>) -> Unit = {},
onFiles: (List<File>) -> Unit = {},
onImage: (Painter) -> Unit = {},
onText: (String) -> Unit = {}
): Modifier

View File

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

View File

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

View File

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

View File

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

View File

@ -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<String>) -> Unit,
onFiles: (List<File>) -> 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())
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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<ImageBitmap, ByteArray>?
}
}
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 {