limit to 1 MB

This commit is contained in:
callebtc
2025-09-27 17:21:14 +02:00
parent 212859aa76
commit fc885acc77
6 changed files with 109 additions and 22 deletions
@@ -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
}
@@ -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 }
}
}
@@ -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() {
@@ -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
*/
@@ -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)
}
}
@@ -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(
)
}
}