From fc885acc779985b79b2d1b1538f8c03b39e1c15f Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 27 Sep 2025 17:21:14 +0200 Subject: [PATCH] limit to 1 MB --- .../features/media/MediaConstraints.kt | 8 +++ .../features/media/MediaSizeLimiter.kt | 68 +++++++++++++++++++ .../com/bitchat/android/ui/ChatViewModel.kt | 14 +--- .../bitchat/android/ui/MediaSendingManager.kt | 32 +++++++-- .../android/ui/media/FilePickerButton.kt | 5 +- .../android/ui/media/ImagePickerButton.kt | 4 +- 6 files changed, 109 insertions(+), 22 deletions(-) create mode 100644 app/src/main/java/com/bitchat/android/features/media/MediaConstraints.kt create mode 100644 app/src/main/java/com/bitchat/android/features/media/MediaSizeLimiter.kt diff --git a/app/src/main/java/com/bitchat/android/features/media/MediaConstraints.kt b/app/src/main/java/com/bitchat/android/features/media/MediaConstraints.kt new file mode 100644 index 00000000..1b068594 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/features/media/MediaConstraints.kt @@ -0,0 +1,8 @@ +package com.bitchat.android.features.media + +object MediaConstraints { + // Global configurable media size limit (in bytes) + // Change this value to adjust the max size for all media transfers + const val MAX_MEDIA_BYTES: Long = 1L * 1024L * 1024L // 1 MB +} + diff --git a/app/src/main/java/com/bitchat/android/features/media/MediaSizeLimiter.kt b/app/src/main/java/com/bitchat/android/features/media/MediaSizeLimiter.kt new file mode 100644 index 00000000..cfc6c3e3 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/features/media/MediaSizeLimiter.kt @@ -0,0 +1,68 @@ +package com.bitchat.android.features.media + +import android.content.Context +import android.net.Uri +import android.provider.OpenableColumns +import android.widget.Toast +import java.io.File + +object MediaSizeLimiter { + private fun formatBytes(bytes: Long): String { + val units = arrayOf("B", "KB", "MB", "GB") + var size = bytes.toDouble() + var unit = 0 + while (size >= 1024 && unit < units.size - 1) { + size /= 1024.0 + unit++ + } + return "%.1f %s".format(size, units[unit]) + } + + private fun toastTooLarge(context: Context, label: String) { + val maxLabel = formatBytes(MediaConstraints.MAX_MEDIA_BYTES) + Toast.makeText(context, "$label is too large to send (max $maxLabel)", Toast.LENGTH_SHORT).show() + } + + fun queryContentLength(context: Context, uri: Uri): Long? { + // Try OpenableColumns.SIZE first + val sizeFromQuery = try { + context.contentResolver.query(uri, arrayOf(OpenableColumns.SIZE), null, null, null)?.use { c -> + val idx = c.getColumnIndex(OpenableColumns.SIZE) + if (idx >= 0 && c.moveToFirst()) c.getLong(idx) else null + } + } catch (_: Exception) { null } + + if (sizeFromQuery != null && sizeFromQuery >= 0) return sizeFromQuery + + // Fallback to file descriptor statSize + val sizeFromFd = try { + context.contentResolver.openFileDescriptor(uri, "r")?.use { it.statSize } + } catch (_: Exception) { null } + + return sizeFromFd?.takeIf { it >= 0 } + } + + // Returns false if too large and shows a toast + fun enforceUriPrecheck(context: Context, uri: Uri, label: String): Boolean { + val len = queryContentLength(context, uri) + if (len != null && len > MediaConstraints.MAX_MEDIA_BYTES) { + toastTooLarge(context, label) + return false + } + return true + } + + // Returns false if too large. Optionally deletes the file if too large. + fun enforcePathPostCheck(context: Context, path: String, label: String, deleteIfTooLarge: Boolean = true): Boolean { + return try { + val file = File(path) + val len = file.length() + if (len > MediaConstraints.MAX_MEDIA_BYTES) { + if (deleteIfTooLarge) runCatching { file.delete() } + toastTooLarge(context, label) + false + } else true + } catch (_: Exception) { true } + } +} + diff --git a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt index 4224d9c1..16af74f2 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt @@ -150,18 +150,8 @@ class ChatViewModel( } fun cancelMediaSend(messageId: String) { - val transferId = synchronized(transferMessageMap) { messageTransferMap[messageId] } - if (transferId != null) { - val cancelled = meshService.cancelFileTransfer(transferId) - if (cancelled) { - // Remove the message from chat upon explicit cancel - messageManager.removeMessageById(messageId) - synchronized(transferMessageMap) { - transferMessageMap.remove(transferId) - messageTransferMap.remove(messageId) - } - } - } + // Delegate to MediaSendingManager which maintains the transferId<->messageId mapping + mediaSendingManager.cancelMediaSend(messageId) } private fun loadAndInitialize() { diff --git a/app/src/main/java/com/bitchat/android/ui/MediaSendingManager.kt b/app/src/main/java/com/bitchat/android/ui/MediaSendingManager.kt index e9befa4e..a5125b08 100644 --- a/app/src/main/java/com/bitchat/android/ui/MediaSendingManager.kt +++ b/app/src/main/java/com/bitchat/android/ui/MediaSendingManager.kt @@ -20,7 +20,6 @@ class MediaSendingManager( ) { companion object { private const val TAG = "MediaSendingManager" - private const val MAX_FILE_SIZE = 50 * 1024 * 1024 // 50MB limit } // Track in-flight transfer progress: transferId -> messageId and reverse @@ -39,8 +38,8 @@ class MediaSendingManager( } Log.d(TAG, "📁 File exists: size=${file.length()} bytes, name=${file.name}") - if (file.length() > MAX_FILE_SIZE) { - Log.e(TAG, "❌ File too large: ${file.length()} bytes (max: $MAX_FILE_SIZE)") + if (file.length() > com.bitchat.android.features.media.MediaConstraints.MAX_MEDIA_BYTES) { + Log.e(TAG, "❌ File too large: ${file.length()} bytes (max: ${com.bitchat.android.features.media.MediaConstraints.MAX_MEDIA_BYTES})") return } @@ -74,8 +73,8 @@ class MediaSendingManager( } Log.d(TAG, "📁 File exists: size=${file.length()} bytes, name=${file.name}") - if (file.length() > MAX_FILE_SIZE) { - Log.e(TAG, "❌ File too large: ${file.length()} bytes (max: $MAX_FILE_SIZE)") + if (file.length() > com.bitchat.android.features.media.MediaConstraints.MAX_MEDIA_BYTES) { + Log.e(TAG, "❌ File too large: ${file.length()} bytes (max: ${com.bitchat.android.features.media.MediaConstraints.MAX_MEDIA_BYTES})") return } @@ -112,8 +111,8 @@ class MediaSendingManager( } Log.d(TAG, "📁 File exists: size=${file.length()} bytes, name=${file.name}") - if (file.length() > MAX_FILE_SIZE) { - Log.e(TAG, "❌ File too large: ${file.length()} bytes (max: $MAX_FILE_SIZE)") + if (file.length() > com.bitchat.android.features.media.MediaConstraints.MAX_MEDIA_BYTES) { + Log.e(TAG, "❌ File too large: ${file.length()} bytes (max: ${com.bitchat.android.features.media.MediaConstraints.MAX_MEDIA_BYTES})") return } @@ -276,6 +275,11 @@ class MediaSendingManager( val cancelled = meshService.cancelFileTransfer(transferId) if (cancelled) { // Remove the message from chat upon explicit cancel + // Also attempt to delete the associated outgoing file + runCatching { findMessagePathById(messageId) }. + getOrNull()?.let { path -> + try { java.io.File(path).takeIf { it.exists() }?.delete() } catch (_: Exception) {} + } messageManager.removeMessageById(messageId) synchronized(transferMessageMap) { transferMessageMap.remove(transferId) @@ -285,6 +289,20 @@ class MediaSendingManager( } } + private fun findMessagePathById(messageId: String): String? { + // Check main messages + state.getMessagesValue().firstOrNull { it.id == messageId }?.content?.let { return it } + // Check private chats + state.getPrivateChatsValue().values.forEach { list -> + list.firstOrNull { it.id == messageId }?.content?.let { return it } + } + // Check channels + state.getChannelMessagesValue().values.forEach { list -> + list.firstOrNull { it.id == messageId }?.content?.let { return it } + } + return null + } + /** * Update progress for a transfer */ diff --git a/app/src/main/java/com/bitchat/android/ui/media/FilePickerButton.kt b/app/src/main/java/com/bitchat/android/ui/media/FilePickerButton.kt index 384e9c9d..de7ddaf0 100644 --- a/app/src/main/java/com/bitchat/android/ui/media/FilePickerButton.kt +++ b/app/src/main/java/com/bitchat/android/ui/media/FilePickerButton.kt @@ -1,6 +1,7 @@ package com.bitchat.android.ui.media import android.net.Uri +import com.bitchat.android.features.media.MediaSizeLimiter import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.size @@ -28,10 +29,12 @@ fun FilePickerButton( contract = ActivityResultContracts.OpenDocument() ) { uri: Uri? -> if (uri != null) { + // Pre-check size via resolver metadata + if (!MediaSizeLimiter.enforceUriPrecheck(context, uri, "File")) return@rememberLauncherForActivityResult // Persist temporary read permission so we can copy try { context.contentResolver.takePersistableUriPermission(uri, android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION) } catch (_: Exception) {} val path = FileUtils.copyFileForSending(context, uri) - if (!path.isNullOrBlank()) onFileReady(path) + if (!path.isNullOrBlank() && MediaSizeLimiter.enforcePathPostCheck(context, path, label = "File", deleteIfTooLarge = true)) onFileReady(path) } } diff --git a/app/src/main/java/com/bitchat/android/ui/media/ImagePickerButton.kt b/app/src/main/java/com/bitchat/android/ui/media/ImagePickerButton.kt index aa3a0b7c..ad30f10e 100644 --- a/app/src/main/java/com/bitchat/android/ui/media/ImagePickerButton.kt +++ b/app/src/main/java/com/bitchat/android/ui/media/ImagePickerButton.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import com.bitchat.android.features.media.ImageUtils +import com.bitchat.android.features.media.MediaSizeLimiter @Composable fun ImagePickerButton( @@ -25,7 +26,7 @@ fun ImagePickerButton( ) { uri: android.net.Uri? -> if (uri != null) { val outPath = ImageUtils.downscaleAndSaveToAppFiles(context, uri) - if (!outPath.isNullOrBlank()) onImageReady(outPath) + if (!outPath.isNullOrBlank() && MediaSizeLimiter.enforcePathPostCheck(context, outPath, label = "Image", deleteIfTooLarge = true)) onImageReady(outPath) } } @@ -41,4 +42,3 @@ fun ImagePickerButton( ) } } -