diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt index 19305c2e51..40c04f5088 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt @@ -16,7 +16,6 @@ import chat.simplex.common.views.chatlist.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.onboarding.* import chat.simplex.common.platform.* -import chat.simplex.res.MR import kotlinx.coroutines.* import java.lang.ref.WeakReference @@ -143,7 +142,7 @@ fun processExternalIntent(intent: Intent?) { val text = intent.getStringExtra(Intent.EXTRA_TEXT) val uri = intent.getParcelableExtra(Intent.EXTRA_STREAM) as? Uri if (uri != null) { - if (uri.scheme != "content") return showNonContentUriAlert() + if (uri.scheme != "content") return showWrongUriAlert() // Shared file that contains plain text, like `*.log` file chatModel.sharedContent.value = SharedContent.File(text ?: "", uri.toURI()) } else if (text != null) { @@ -154,14 +153,14 @@ fun processExternalIntent(intent: Intent?) { isMediaIntent(intent) -> { val uri = intent.getParcelableExtra(Intent.EXTRA_STREAM) as? Uri if (uri != null) { - if (uri.scheme != "content") return showNonContentUriAlert() + if (uri.scheme != "content") return showWrongUriAlert() chatModel.sharedContent.value = SharedContent.Media(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", listOf(uri.toURI())) } // All other mime types } else -> { val uri = intent.getParcelableExtra(Intent.EXTRA_STREAM) as? Uri if (uri != null) { - if (uri.scheme != "content") return showNonContentUriAlert() + if (uri.scheme != "content") return showWrongUriAlert() chatModel.sharedContent.value = SharedContent.File(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", uri.toURI()) } } @@ -176,7 +175,7 @@ fun processExternalIntent(intent: Intent?) { isMediaIntent(intent) -> { val uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM) as? List if (uris != null) { - if (uris.any { it.scheme != "content" }) return showNonContentUriAlert() + if (uris.any { it.scheme != "content" }) return showWrongUriAlert() chatModel.sharedContent.value = SharedContent.Media(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", uris.map { it.toURI() }) } // All other mime types } @@ -189,13 +188,6 @@ fun processExternalIntent(intent: Intent?) { fun isMediaIntent(intent: Intent): Boolean = intent.type?.startsWith("image/") == true || intent.type?.startsWith("video/") == true -private fun showNonContentUriAlert() { - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.non_content_uri_alert_title), - text = generalGetString(MR.strings.non_content_uri_alert_text) - ) -} - //fun testJson() { // val str: String = """ // """.trimIndent() 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 9d5eadad72..574b756bb4 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 @@ -200,7 +200,7 @@ actual class VideoPlayer actual constructor( private fun setPreviewAndDuration() { // It freezes main thread, doing it in IO thread CoroutineScope(Dispatchers.IO).launch { - val previewAndDuration = VideoPlayerHolder.previewsAndDurations.getOrPut(uri) { getBitmapFromVideo(uri) } + val previewAndDuration = VideoPlayerHolder.previewsAndDurations.getOrPut(uri) { getBitmapFromVideo(uri, withAlertOnException = false) } withContext(Dispatchers.Main) { preview.value = previewAndDuration.preview ?: defaultPreview duration.value = (previewAndDuration.duration ?: 0) diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt index 18442d7791..8b98b05421 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt @@ -233,17 +233,13 @@ actual fun getFileSize(uri: URI): Long? { actual fun getBitmapFromUri(uri: URI, withAlertOnException: Boolean): ImageBitmap? { return if (Build.VERSION.SDK_INT >= 28) { - val source = ImageDecoder.createSource(androidAppContext.contentResolver, uri.toUri()) try { + val source = ImageDecoder.createSource(androidAppContext.contentResolver, uri.toUri()) ImageDecoder.decodeBitmap(source) - } catch (e: android.graphics.ImageDecoder.DecodeException) { + } catch (e: Exception) { Log.e(TAG, "Unable to decode the image: ${e.stackTraceToString()}") - if (withAlertOnException) { - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.image_decoding_exception_title), - text = generalGetString(MR.strings.image_decoding_exception_desc) - ) - } + if (withAlertOnException) showImageDecodingException() + null } } else { @@ -253,17 +249,13 @@ actual fun getBitmapFromUri(uri: URI, withAlertOnException: Boolean): ImageBitma actual fun getBitmapFromByteArray(data: ByteArray, withAlertOnException: Boolean): ImageBitmap? { return if (Build.VERSION.SDK_INT >= 31) { - val source = ImageDecoder.createSource(data) try { + val source = ImageDecoder.createSource(data) ImageDecoder.decodeBitmap(source) } catch (e: android.graphics.ImageDecoder.DecodeException) { Log.e(TAG, "Unable to decode the image: ${e.stackTraceToString()}") - if (withAlertOnException) { - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.image_decoding_exception_title), - text = generalGetString(MR.strings.image_decoding_exception_desc) - ) - } + if (withAlertOnException) showImageDecodingException() + null } } else { @@ -273,17 +265,13 @@ actual fun getBitmapFromByteArray(data: ByteArray, withAlertOnException: Boolean actual fun getDrawableFromUri(uri: URI, withAlertOnException: Boolean): Any? { return if (Build.VERSION.SDK_INT >= 28) { - val source = ImageDecoder.createSource(androidAppContext.contentResolver, uri.toUri()) try { + val source = ImageDecoder.createSource(androidAppContext.contentResolver, uri.toUri()) ImageDecoder.decodeDrawable(source) - } catch (e: android.graphics.ImageDecoder.DecodeException) { - if (withAlertOnException) { - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.image_decoding_exception_title), - text = generalGetString(MR.strings.image_decoding_exception_desc) - ) - } + } catch (e: Exception) { Log.e(TAG, "Error while decoding drawable: ${e.stackTraceToString()}") + if (withAlertOnException) showImageDecodingException() + null } } else { @@ -304,23 +292,29 @@ actual suspend fun saveTempImageUncompressed(image: ImageBitmap, asPng: Boolean) ChatModel.filesToDelete.add(this) } } catch (e: Exception) { - Log.e(TAG, "Util.kt saveTempImageUncompressed error: ${e.message}") + Log.e(TAG, "Utils.android saveTempImageUncompressed error: ${e.message}") null } } -actual suspend fun getBitmapFromVideo(uri: URI, timestamp: Long?, random: Boolean): VideoPlayerInterface.PreviewAndDuration { - val mmr = MediaMetadataRetriever() - mmr.setDataSource(androidAppContext, uri.toUri()) - val durationMs = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() - val image = when { - timestamp != null -> mmr.getFrameAtTime(timestamp * 1000, MediaMetadataRetriever.OPTION_CLOSEST) - random -> mmr.frameAtTime - else -> mmr.getFrameAtTime(0) +actual suspend fun getBitmapFromVideo(uri: URI, timestamp: Long?, random: Boolean, withAlertOnException: Boolean): VideoPlayerInterface.PreviewAndDuration = + try { + val mmr = MediaMetadataRetriever() + mmr.setDataSource(androidAppContext, uri.toUri()) + val durationMs = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() + val image = when { + timestamp != null -> mmr.getFrameAtTime(timestamp * 1000, MediaMetadataRetriever.OPTION_CLOSEST) + random -> mmr.frameAtTime + else -> mmr.getFrameAtTime(0) + } + mmr.release() + VideoPlayerInterface.PreviewAndDuration(image?.asImageBitmap(), durationMs, timestamp ?: 0) + } catch (e: Exception) { + Log.e(TAG, "Utils.android getBitmapFromVideo error: ${e.message}") + if (withAlertOnException) showVideoDecodingException() + + VideoPlayerInterface.PreviewAndDuration(null, 0, 0) } - mmr.release() - return VideoPlayerInterface.PreviewAndDuration(image?.asImageBitmap(), durationMs, timestamp ?: 0) -} actual fun ByteArray.toBase64StringForPassphrase(): String = Base64.encodeToString(this, Base64.DEFAULT) 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 84a5879b2b..59e43557e7 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 @@ -178,11 +178,13 @@ fun MutableState.processPickedFile(uri: URI?, text: String?) { if (fileName != null) { value = value.copy(message = text ?: value.message, preview = ComposePreview.FilePreview(fileName, uri)) } - } else { + } else if (fileSize != null) { AlertManager.shared.showAlertMsg( generalGetString(MR.strings.large_file), String.format(generalGetString(MR.strings.maximum_supported_file_size), formatBytes(maxFileSize)) ) + } else { + showWrongUriAlert() } } } @@ -196,7 +198,8 @@ suspend fun MutableState.processPickedMedia(uris: List, text: isImage(uri) -> { // Image val drawable = getDrawableFromUri(uri) - bitmap = getBitmapFromUri(uri) + // Do not show alert in case it's already shown from the function above + bitmap = getBitmapFromUri(uri, withAlertOnException = AlertManager.shared.alertViews.isEmpty()) if (isAnimImage(uri, drawable)) { // It's a gif or webp val fileSize = getFileSize(uri) @@ -209,13 +212,13 @@ suspend fun MutableState.processPickedMedia(uris: List, text: String.format(generalGetString(MR.strings.maximum_supported_file_size), formatBytes(maxFileSize)) ) } - } else { + } else if (bitmap != null) { content.add(UploadContent.SimpleImage(uri)) } } else -> { // Video - val res = getBitmapFromVideo(uri) + val res = getBitmapFromVideo(uri, withAlertOnException = true) bitmap = res.preview val durationMs = res.duration content.add(UploadContent.Video(uri, durationMs?.div(1000)?.toInt() ?: 0)) 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 2aad0bc3d0..dae79e6fc0 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 @@ -151,7 +151,7 @@ fun saveAnimImage(uri: URI, encrypted: Boolean): CryptoFile? { expect suspend fun saveTempImageUncompressed(image: ImageBitmap, asPng: Boolean): File? -fun saveFileFromUri(uri: URI, encrypted: Boolean): CryptoFile? { +fun saveFileFromUri(uri: URI, encrypted: Boolean, withAlertOnException: Boolean = true): CryptoFile? { return try { val inputStream = uri.inputStream() val fileToSave = getFileName(uri) @@ -170,10 +170,14 @@ fun saveFileFromUri(uri: URI, encrypted: Boolean): CryptoFile? { } } else { Log.e(TAG, "Util.kt saveFileFromUri null inputStream") + if (withAlertOnException) showWrongUriAlert() + null } } catch (e: Exception) { Log.e(TAG, "Util.kt saveFileFromUri error: ${e.stackTraceToString()}") + if (withAlertOnException) showWrongUriAlert() + null } } @@ -267,7 +271,28 @@ fun getMaxFileSize(fileProtocol: FileProtocol): Long { } } -expect suspend fun getBitmapFromVideo(uri: URI, timestamp: Long? = null, random: Boolean = true): VideoPlayerInterface.PreviewAndDuration +expect suspend fun getBitmapFromVideo(uri: URI, timestamp: Long? = null, random: Boolean = true, withAlertOnException: Boolean = true): VideoPlayerInterface.PreviewAndDuration + +fun showWrongUriAlert() { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.non_content_uri_alert_title), + text = generalGetString(MR.strings.non_content_uri_alert_text) + ) +} + +fun showImageDecodingException() { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.image_decoding_exception_title), + text = generalGetString(MR.strings.image_decoding_exception_desc) + ) +} + +fun showVideoDecodingException() { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.image_decoding_exception_title), + text = generalGetString(MR.strings.video_decoding_exception_desc) + ) +} fun Color.darker(factor: Float = 0.1f): Color = Color(max(red * (1 - factor), 0f), max(green * (1 - factor), 0f), max(blue * (1 - factor), 0f), alpha) diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 9df3f9d621..ff34934e41 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -309,6 +309,7 @@ Only 10 videos can be sent at the same time Decoding error The image cannot be decoded. Please, try a different image or contact developers. + The video cannot be decoded. Please, try a different video or contact developers. you are observer You can\'t send messages! Please contact group admin. 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 9f8c1884eb..fa0a8247a4 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 @@ -189,7 +189,7 @@ actual class VideoPlayer actual constructor( private fun setPreviewAndDuration() { // It freezes main thread, doing it in IO thread CoroutineScope(Dispatchers.IO).launch { - val previewAndDuration = VideoPlayerHolder.previewsAndDurations.getOrPut(uri) { getBitmapFromVideo(defaultPreview, uri) } + val previewAndDuration = VideoPlayerHolder.previewsAndDurations.getOrPut(uri) { getBitmapFromVideo(defaultPreview, uri, withAlertOnException = false) } withContext(Dispatchers.Main) { preview.value = previewAndDuration.preview ?: defaultPreview duration.value = (previewAndDuration.duration ?: 0) @@ -214,10 +214,12 @@ actual class VideoPlayer actual constructor( } } - suspend fun getBitmapFromVideo(defaultPreview: ImageBitmap?, uri: URI?): VideoPlayerInterface.PreviewAndDuration = withContext(playerThread.asCoroutineDispatcher()) { + 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 (withAlertOnException) showVideoDecodingException() + return@withContext VideoPlayerInterface.PreviewAndDuration(preview = defaultPreview, timestamp = 0L, duration = 0L) } player.media().startPaused(uri.toString().replaceFirst("file:", "file://")) @@ -227,7 +229,14 @@ actual class VideoPlayer actual constructor( snap = player.snapshots()?.get() delay(10) } - val orientation = player.media().info().videoTracks().first().orientation() + val orientation = player.media().info().videoTracks().firstOrNull()?.orientation() + if (orientation == null) { + player.stop() + putHelperPlayer(mediaComponent) + if (withAlertOnException) showVideoDecodingException() + + return@withContext VideoPlayerInterface.PreviewAndDuration(preview = defaultPreview, timestamp = 0L, duration = 0L) + } val preview: ImageBitmap? = when (orientation) { VideoOrientation.TOP_LEFT -> snap VideoOrientation.TOP_RIGHT -> snap?.flip(false, true) 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 2a9042c433..e867dd1b34 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 @@ -9,6 +9,7 @@ import chat.simplex.common.model.CIFile import chat.simplex.common.model.readCryptoFile 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 @@ -108,10 +109,24 @@ actual fun getAppFilePath(uri: URI): String? = uri.path actual fun getFileSize(uri: URI): Long? = uri.toPath().toFile().length() actual fun getBitmapFromUri(uri: URI, withAlertOnException: Boolean): ImageBitmap? = - ImageIO.read(uri.inputStream()).toComposeImageBitmap() + try { + ImageIO.read(uri.inputStream()).toComposeImageBitmap() + } catch (e: Exception) { + Log.e(TAG, "Error while decoding drawable: ${e.stackTraceToString()}") + if (withAlertOnException) showImageDecodingException() + + null + } actual fun getBitmapFromByteArray(data: ByteArray, withAlertOnException: Boolean): ImageBitmap? = - ImageIO.read(ByteArrayInputStream(data)).toComposeImageBitmap() + try { + ImageIO.read(ByteArrayInputStream(data)).toComposeImageBitmap() + } catch (e: Exception) { + Log.e(TAG, "Error while encoding bitmap from byte array: ${e.stackTraceToString()}") + if (withAlertOnException) showImageDecodingException() + + null + } // LALAL implement to support animated drawable actual fun getDrawableFromUri(uri: URI, withAlertOnException: Boolean): Any? = null @@ -132,8 +147,8 @@ actual suspend fun saveTempImageUncompressed(image: ImageBitmap, asPng: Boolean) } else null } -actual suspend fun getBitmapFromVideo(uri: URI, timestamp: Long?, random: Boolean): VideoPlayerInterface.PreviewAndDuration { - return VideoPlayer.getBitmapFromVideo(null, uri) +actual suspend fun getBitmapFromVideo(uri: URI, timestamp: Long?, random: Boolean, withAlertOnException: Boolean): VideoPlayerInterface.PreviewAndDuration { + return VideoPlayer.getBitmapFromVideo(null, uri, withAlertOnException) } @OptIn(ExperimentalEncodingApi::class)