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 19305c2e5..40c04f508 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/build.gradle.kts b/apps/multiplatform/build.gradle.kts index 9d6fd0c20..f40420752 100644 --- a/apps/multiplatform/build.gradle.kts +++ b/apps/multiplatform/build.gradle.kts @@ -45,7 +45,6 @@ buildscript { dependencies { classpath("com.android.tools.build:gradle:${rootProject.extra["gradle.plugin.version"]}") classpath(kotlin("gradle-plugin", version = rootProject.extra["kotlin.version"] as String)) - classpath("org.jetbrains.kotlin:kotlin-serialization:1.3.2") classpath("dev.icerock.moko:resources-generator:0.23.0") // NOTE: Do not place your application dependencies here; they belong diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Share.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Share.android.kt index a370bbf40..cf3fcbaae 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Share.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Share.android.kt @@ -36,7 +36,7 @@ actual fun shareFile(text: String, fileSource: CryptoFile) { tmpFile.deleteOnExit() ChatModel.filesToDelete.add(tmpFile) decryptCryptoFile(getAppFilePath(fileSource.filePath), fileSource.cryptoArgs, tmpFile.absolutePath) - FileProvider.getUriForFile(androidAppContext, "$APPLICATION_ID.provider", File(tmpFile.absolutePath)).toURI() + getAppFileUri(tmpFile.absolutePath) } else { getAppFileUri(fileSource.filePath) } 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 9d5eadad7..574b756bb 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 127f13cd5..8b98b0542 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 @@ -152,7 +152,7 @@ private fun spannableStringToAnnotatedString( } actual fun getAppFileUri(fileName: String): URI = - FileProvider.getUriForFile(androidAppContext, "$APPLICATION_ID.provider", File(getAppFilePath(fileName))).toURI() + FileProvider.getUriForFile(androidAppContext, "$APPLICATION_ID.provider", if (File(fileName).isAbsolute) File(fileName) else File(getAppFilePath(fileName))).toURI() // https://developer.android.com/training/data-storage/shared/documents-files#bitmap actual fun getLoadedImage(file: CIFile?): Pair? { @@ -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/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index e724dfd7c..2e46a1387 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 @@ -589,18 +589,30 @@ fun ChatInfoToolbar( if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.allowsFeature(ChatFeature.Calls)) { if (activeCall == null) { barButtons.add { - IconButton( - { + if (appPlatform.isAndroid) { + IconButton({ showMenu.value = false startCall(CallMediaType.Audio) - }, - enabled = chat.chatInfo.contact.ready && chat.chatInfo.contact.active - ) { - Icon( - painterResource(MR.images.ic_call_500), - stringResource(MR.strings.icon_descr_more_button), - tint = if (chat.chatInfo.contact.ready && chat.chatInfo.contact.active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary - ) + }, enabled = chat.chatInfo.contact.ready && chat.chatInfo.contact.active + ) { + Icon( + painterResource(MR.images.ic_call_500), + stringResource(MR.strings.icon_descr_audio_call).capitalize(Locale.current), + tint = if (chat.chatInfo.contact.ready && chat.chatInfo.contact.active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary + ) + } + } else { + IconButton({ + showMenu.value = false + startCall(CallMediaType.Video) + }, enabled = chat.chatInfo.contact.ready && chat.chatInfo.contact.active + ) { + Icon( + painterResource(MR.images.ic_videocam), + stringResource(MR.strings.icon_descr_video_call).capitalize(Locale.current), + tint = if (chat.chatInfo.contact.ready && chat.chatInfo.contact.active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary + ) + } } } } else if (activeCall?.contact?.id == chat.id) { @@ -634,10 +646,17 @@ fun ChatInfoToolbar( } if (chat.chatInfo.contact.ready && chat.chatInfo.contact.active && activeCall == null) { menuItems.add { - ItemAction(stringResource(MR.strings.icon_descr_video_call).capitalize(Locale.current), painterResource(MR.images.ic_videocam), onClick = { - showMenu.value = false - startCall(CallMediaType.Video) - }) + if (appPlatform.isAndroid) { + ItemAction(stringResource(MR.strings.icon_descr_video_call).capitalize(Locale.current), painterResource(MR.images.ic_videocam), onClick = { + showMenu.value = false + startCall(CallMediaType.Video) + }) + } else { + ItemAction(stringResource(MR.strings.icon_descr_audio_call).capitalize(Locale.current), painterResource(MR.images.ic_call_500), onClick = { + showMenu.value = false + startCall(CallMediaType.Audio) + }) + } } } } else if (chat.chatInfo is ChatInfo.Group && chat.chatInfo.groupInfo.canAddMembers) { 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 84a5879b2..59e43557e 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 2aad0bc3d..dae79e6fc 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 9df3f9d62..ff34934e4 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/commonMain/resources/assets/www/android/call.html b/apps/multiplatform/common/src/commonMain/resources/assets/www/android/call.html index 46910bfaf..7b51a0515 100644 --- a/apps/multiplatform/common/src/commonMain/resources/assets/www/android/call.html +++ b/apps/multiplatform/common/src/commonMain/resources/assets/www/android/call.html @@ -11,6 +11,7 @@ autoplay playsinline poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII=" + onclick="javascript:toggleRemoteVideoFitFill()" >