feat: Implement media picker and camera functionality

- Added permissions for CAMERA, READ_EXTERNAL_STORAGE, READ_MEDIA_IMAGES, and READ_MEDIA_VIDEO in AndroidManifest.xml.
- Introduced MediaPickerBottomSheet for selecting images and videos from the gallery.
- Implemented camera functionality to capture images and send them in chat.
- Created MediaUtils for handling image and file conversions to Base64.
- Updated ChatDetailScreen to handle media selection and sending images/files.
- Enhanced ChatViewModel with methods to send image and file messages.
- Added file_paths.xml for FileProvider configuration.
This commit is contained in:
2026-01-25 19:19:31 +05:00
parent 89746c5bbd
commit 636cd9f3b8
7 changed files with 1327 additions and 3 deletions

View File

@@ -0,0 +1,219 @@
package com.rosetta.messenger.utils
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.util.Base64
import android.util.Log
import com.vanniktech.blurhash.BlurHash
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.ByteArrayOutputStream
import java.io.InputStream
private const val TAG = "MediaUtils"
/**
* Утилиты для работы с медиафайлами
* Конвертация изображений в Base64, генерация blurhash
*/
object MediaUtils {
// Максимальный размер изображения для отправки (сжимаем большие изображения)
private const val MAX_IMAGE_SIZE = 1920
private const val IMAGE_QUALITY = 85
// Максимальный размер файла в МБ
const val MAX_FILE_SIZE_MB = 15
/**
* Конвертировать изображение из Uri в Base64 PNG
* Автоматически сжимает большие изображения
*/
suspend fun uriToBase64Image(context: Context, uri: Uri): String? = withContext(Dispatchers.IO) {
try {
Log.d(TAG, "📸 Converting image to Base64: $uri")
// Открываем InputStream
val inputStream: InputStream = context.contentResolver.openInputStream(uri)
?: return@withContext null
// Декодируем изображение
val originalBitmap = BitmapFactory.decodeStream(inputStream)
inputStream.close()
if (originalBitmap == null) {
Log.e(TAG, "📸 Failed to decode image")
return@withContext null
}
Log.d(TAG, "📸 Original size: ${originalBitmap.width}x${originalBitmap.height}")
// Масштабируем если слишком большое
val scaledBitmap = scaleDownBitmap(originalBitmap, MAX_IMAGE_SIZE)
if (scaledBitmap != originalBitmap) {
originalBitmap.recycle()
}
Log.d(TAG, "📸 Scaled size: ${scaledBitmap.width}x${scaledBitmap.height}")
// Конвертируем в PNG Base64
val outputStream = ByteArrayOutputStream()
scaledBitmap.compress(Bitmap.CompressFormat.PNG, IMAGE_QUALITY, outputStream)
val bytes = outputStream.toByteArray()
val base64 = "data:image/png;base64," + Base64.encodeToString(bytes, Base64.NO_WRAP)
scaledBitmap.recycle()
Log.d(TAG, "📸 ✅ Image converted to Base64, length: ${base64.length}")
base64
} catch (e: Exception) {
Log.e(TAG, "📸 ❌ Failed to convert image to Base64", e)
null
}
}
/**
* Генерировать Blurhash для изображения
*/
suspend fun generateBlurhash(context: Context, uri: Uri): String = withContext(Dispatchers.IO) {
try {
Log.d(TAG, "🎨 Generating blurhash for: $uri")
val inputStream = context.contentResolver.openInputStream(uri)
?: return@withContext ""
// Декодируем в маленький размер для blurhash
val options = BitmapFactory.Options().apply {
inSampleSize = 8 // Уменьшаем в 8 раз для быстрого расчета
}
val bitmap = BitmapFactory.decodeStream(inputStream, null, options)
inputStream.close()
if (bitmap == null) {
Log.e(TAG, "🎨 Failed to decode image for blurhash")
return@withContext ""
}
// Генерируем blurhash
val blurhash = BlurHash.encode(bitmap, 4, 3)
bitmap.recycle()
Log.d(TAG, "🎨 ✅ Blurhash generated: $blurhash")
blurhash ?: ""
} catch (e: Exception) {
Log.e(TAG, "🎨 ❌ Failed to generate blurhash", e)
""
}
}
/**
* Конвертировать файл из Uri в Base64
*/
suspend fun uriToBase64File(context: Context, uri: Uri): String? = withContext(Dispatchers.IO) {
try {
Log.d(TAG, "📄 Converting file to Base64: $uri")
val inputStream = context.contentResolver.openInputStream(uri)
?: return@withContext null
val bytes = inputStream.readBytes()
inputStream.close()
val base64 = Base64.encodeToString(bytes, Base64.NO_WRAP)
Log.d(TAG, "📄 ✅ File converted to Base64, length: ${base64.length}")
base64
} catch (e: Exception) {
Log.e(TAG, "📄 ❌ Failed to convert file to Base64", e)
null
}
}
/**
* Получить размер файла из Uri
*/
fun getFileSize(context: Context, uri: Uri): Long {
return try {
context.contentResolver.openInputStream(uri)?.use {
it.available().toLong()
} ?: 0L
} catch (e: Exception) {
0L
}
}
/**
* Получить имя файла из Uri
*/
fun getFileName(context: Context, uri: Uri): String {
var name = "file"
try {
context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
val nameIndex = cursor.getColumnIndex(android.provider.OpenableColumns.DISPLAY_NAME)
if (nameIndex != -1 && cursor.moveToFirst()) {
name = cursor.getString(nameIndex) ?: "file"
}
}
} catch (e: Exception) {
Log.e(TAG, "Failed to get file name", e)
}
return name
}
/**
* Масштабировать bitmap до максимального размера
*/
private fun scaleDownBitmap(bitmap: Bitmap, maxSize: Int): Bitmap {
val width = bitmap.width
val height = bitmap.height
if (width <= maxSize && height <= maxSize) {
return bitmap
}
val ratio = width.toFloat() / height.toFloat()
val newWidth: Int
val newHeight: Int
if (width > height) {
newWidth = maxSize
newHeight = (maxSize / ratio).toInt()
} else {
newHeight = maxSize
newWidth = (maxSize * ratio).toInt()
}
return Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true)
}
/**
* Конвертировать Bitmap в Base64 PNG
*/
fun bitmapToBase64(bitmap: Bitmap): String {
val outputStream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.PNG, IMAGE_QUALITY, outputStream)
val bytes = outputStream.toByteArray()
return "data:image/png;base64," + Base64.encodeToString(bytes, Base64.NO_WRAP)
}
/**
* Генерировать Blurhash из Bitmap
*/
fun generateBlurhashFromBitmap(bitmap: Bitmap): String {
return try {
// Уменьшаем для быстрого расчета
val scaledBitmap = Bitmap.createScaledBitmap(bitmap, 32, 32, true)
val hash = BlurHash.encode(scaledBitmap, 4, 3)
if (scaledBitmap != bitmap) {
scaledBitmap.recycle()
}
hash ?: ""
} catch (e: Exception) {
Log.e(TAG, "Failed to generate blurhash from bitmap", e)
""
}
}
}