mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 18:45:20 +00:00
limit to 1 MB
This commit is contained in:
@@ -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) {
|
fun cancelMediaSend(messageId: String) {
|
||||||
val transferId = synchronized(transferMessageMap) { messageTransferMap[messageId] }
|
// Delegate to MediaSendingManager which maintains the transferId<->messageId mapping
|
||||||
if (transferId != null) {
|
mediaSendingManager.cancelMediaSend(messageId)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadAndInitialize() {
|
private fun loadAndInitialize() {
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ class MediaSendingManager(
|
|||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "MediaSendingManager"
|
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
|
// 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}")
|
Log.d(TAG, "📁 File exists: size=${file.length()} bytes, name=${file.name}")
|
||||||
|
|
||||||
if (file.length() > 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: $MAX_FILE_SIZE)")
|
Log.e(TAG, "❌ File too large: ${file.length()} bytes (max: ${com.bitchat.android.features.media.MediaConstraints.MAX_MEDIA_BYTES})")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,8 +73,8 @@ class MediaSendingManager(
|
|||||||
}
|
}
|
||||||
Log.d(TAG, "📁 File exists: size=${file.length()} bytes, name=${file.name}")
|
Log.d(TAG, "📁 File exists: size=${file.length()} bytes, name=${file.name}")
|
||||||
|
|
||||||
if (file.length() > 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: $MAX_FILE_SIZE)")
|
Log.e(TAG, "❌ File too large: ${file.length()} bytes (max: ${com.bitchat.android.features.media.MediaConstraints.MAX_MEDIA_BYTES})")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,8 +111,8 @@ class MediaSendingManager(
|
|||||||
}
|
}
|
||||||
Log.d(TAG, "📁 File exists: size=${file.length()} bytes, name=${file.name}")
|
Log.d(TAG, "📁 File exists: size=${file.length()} bytes, name=${file.name}")
|
||||||
|
|
||||||
if (file.length() > 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: $MAX_FILE_SIZE)")
|
Log.e(TAG, "❌ File too large: ${file.length()} bytes (max: ${com.bitchat.android.features.media.MediaConstraints.MAX_MEDIA_BYTES})")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,6 +275,11 @@ class MediaSendingManager(
|
|||||||
val cancelled = meshService.cancelFileTransfer(transferId)
|
val cancelled = meshService.cancelFileTransfer(transferId)
|
||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
// Remove the message from chat upon explicit cancel
|
// 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)
|
messageManager.removeMessageById(messageId)
|
||||||
synchronized(transferMessageMap) {
|
synchronized(transferMessageMap) {
|
||||||
transferMessageMap.remove(transferId)
|
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
|
* Update progress for a transfer
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.bitchat.android.ui.media
|
package com.bitchat.android.ui.media
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import com.bitchat.android.features.media.MediaSizeLimiter
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
@@ -28,10 +29,12 @@ fun FilePickerButton(
|
|||||||
contract = ActivityResultContracts.OpenDocument()
|
contract = ActivityResultContracts.OpenDocument()
|
||||||
) { uri: Uri? ->
|
) { uri: Uri? ->
|
||||||
if (uri != null) {
|
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
|
// Persist temporary read permission so we can copy
|
||||||
try { context.contentResolver.takePersistableUriPermission(uri, android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION) } catch (_: Exception) {}
|
try { context.contentResolver.takePersistableUriPermission(uri, android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION) } catch (_: Exception) {}
|
||||||
val path = FileUtils.copyFileForSending(context, uri)
|
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.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.bitchat.android.features.media.ImageUtils
|
import com.bitchat.android.features.media.ImageUtils
|
||||||
|
import com.bitchat.android.features.media.MediaSizeLimiter
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ImagePickerButton(
|
fun ImagePickerButton(
|
||||||
@@ -25,7 +26,7 @@ fun ImagePickerButton(
|
|||||||
) { uri: android.net.Uri? ->
|
) { uri: android.net.Uri? ->
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
val outPath = ImageUtils.downscaleAndSaveToAppFiles(context, uri)
|
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(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user