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:
219
app/src/main/java/com/rosetta/messenger/utils/MediaUtils.kt
Normal file
219
app/src/main/java/com/rosetta/messenger/utils/MediaUtils.kt
Normal 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)
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user