feat: Enhance background color animation on theme change in SwipeableDialogItem
This commit is contained in:
@@ -3,10 +3,13 @@ package com.rosetta.messenger.utils
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.ImageDecoder
|
||||
import android.graphics.Matrix
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Base64
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import com.rosetta.messenger.network.ProtocolManager
|
||||
import com.vanniktech.blurhash.BlurHash
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -28,6 +31,10 @@ object MediaUtils {
|
||||
// Android ограничение: файл + base64 + шифрование = ~3x памяти
|
||||
// 20 МБ файл = ~60 МБ RAM, безопасно для большинства устройств
|
||||
const val MAX_FILE_SIZE_MB = 20
|
||||
|
||||
private fun logImage(message: String) {
|
||||
ProtocolManager.addLog("🧪 IMG-UTIL | $message")
|
||||
}
|
||||
|
||||
/**
|
||||
* Конвертировать изображение из Uri в Base64 PNG
|
||||
@@ -35,8 +42,12 @@ object MediaUtils {
|
||||
*/
|
||||
suspend fun uriToBase64Image(context: Context, uri: Uri): String? = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val uriInfo = "${uri.scheme ?: "unknown"}:${uri.lastPathSegment ?: "unknown"}"
|
||||
logImage("encode start: $uriInfo")
|
||||
|
||||
// Читаем EXIF ориентацию
|
||||
val orientation = getExifOrientation(context, uri)
|
||||
logImage("orientation=$orientation")
|
||||
|
||||
val boundsOptions =
|
||||
BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||
@@ -45,47 +56,117 @@ object MediaUtils {
|
||||
} ?: return@withContext null
|
||||
|
||||
if (boundsOptions.outWidth <= 0 || boundsOptions.outHeight <= 0) {
|
||||
logImage("bounds decode failed, trying direct decode fallback")
|
||||
}
|
||||
|
||||
val sourceWidth = boundsOptions.outWidth.coerceAtLeast(1)
|
||||
val sourceHeight = boundsOptions.outHeight.coerceAtLeast(1)
|
||||
val initialSample =
|
||||
calculateInSampleSize(
|
||||
sourceWidth,
|
||||
sourceHeight,
|
||||
MAX_IMAGE_SIZE * 2
|
||||
)
|
||||
|
||||
var bitmap: Bitmap? = null
|
||||
var sample = initialSample.coerceAtLeast(1)
|
||||
|
||||
repeat(2) { attempt ->
|
||||
if (bitmap != null) return@repeat
|
||||
val decodeOptions =
|
||||
BitmapFactory.Options().apply {
|
||||
inSampleSize = sample
|
||||
inPreferredConfig = Bitmap.Config.ARGB_8888
|
||||
}
|
||||
|
||||
bitmap =
|
||||
context.contentResolver.openInputStream(uri)?.use { inputStream ->
|
||||
BitmapFactory.decodeStream(inputStream, null, decodeOptions)
|
||||
}
|
||||
|
||||
if (bitmap == null) {
|
||||
bitmap = context.contentResolver.openFileDescriptor(uri, "r")?.use { pfd ->
|
||||
BitmapFactory.decodeFileDescriptor(pfd.fileDescriptor, null, decodeOptions)
|
||||
}
|
||||
}
|
||||
|
||||
if (bitmap == null) {
|
||||
logImage("decode attempt ${attempt + 1} failed (sample=$sample)")
|
||||
sample *= 2
|
||||
}
|
||||
}
|
||||
|
||||
if (bitmap == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
bitmap = decodeWithImageDecoder(context, uri)
|
||||
if (bitmap != null) {
|
||||
logImage("decoded via ImageDecoder fallback")
|
||||
}
|
||||
}
|
||||
|
||||
if (bitmap == null) {
|
||||
logImage("decode failed after all fallbacks")
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
val decodeOptions =
|
||||
BitmapFactory.Options().apply {
|
||||
inSampleSize =
|
||||
calculateInSampleSize(
|
||||
boundsOptions.outWidth,
|
||||
boundsOptions.outHeight,
|
||||
MAX_IMAGE_SIZE * 2
|
||||
)
|
||||
inPreferredConfig = Bitmap.Config.ARGB_8888
|
||||
val decodedBitmap = bitmap
|
||||
?: run {
|
||||
logImage("decode failed: bitmap is null after fallbacks")
|
||||
return@withContext null
|
||||
}
|
||||
var bitmap =
|
||||
context.contentResolver.openInputStream(uri)?.use { inputStream ->
|
||||
BitmapFactory.decodeStream(inputStream, null, decodeOptions)
|
||||
} ?: return@withContext null
|
||||
|
||||
// Применяем EXIF ориентацию (поворот/отражение)
|
||||
bitmap = applyExifOrientation(bitmap, orientation)
|
||||
val orientedBitmap = applyExifOrientation(decodedBitmap, orientation)
|
||||
|
||||
// Масштабируем если слишком большое
|
||||
val scaledBitmap = scaleDownBitmap(bitmap, MAX_IMAGE_SIZE)
|
||||
if (scaledBitmap != bitmap) {
|
||||
bitmap.recycle()
|
||||
val scaledBitmap = scaleDownBitmap(orientedBitmap, MAX_IMAGE_SIZE)
|
||||
if (scaledBitmap != orientedBitmap) {
|
||||
orientedBitmap.recycle()
|
||||
}
|
||||
|
||||
|
||||
// Конвертируем в PNG Base64
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
scaledBitmap.compress(Bitmap.CompressFormat.PNG, IMAGE_QUALITY, outputStream)
|
||||
val compressed = scaledBitmap.compress(Bitmap.CompressFormat.PNG, IMAGE_QUALITY, outputStream)
|
||||
if (!compressed) {
|
||||
logImage("bitmap compress failed")
|
||||
scaledBitmap.recycle()
|
||||
return@withContext null
|
||||
}
|
||||
val bytes = outputStream.toByteArray()
|
||||
|
||||
val base64 = "data:image/png;base64," + Base64.encodeToString(bytes, Base64.NO_WRAP)
|
||||
|
||||
scaledBitmap.recycle()
|
||||
logImage("encode success: outBytes=${bytes.size}")
|
||||
|
||||
base64
|
||||
} catch (e: Exception) {
|
||||
logImage("encode exception: ${e.javaClass.simpleName}: ${e.message ?: "unknown"}")
|
||||
null
|
||||
} catch (e: OutOfMemoryError) {
|
||||
logImage("encode OOM: ${e.message ?: "unknown"}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun decodeWithImageDecoder(context: Context, uri: Uri): Bitmap? {
|
||||
return try {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) return null
|
||||
val source = ImageDecoder.createSource(context.contentResolver, uri)
|
||||
ImageDecoder.decodeBitmap(source) { decoder, info, _ ->
|
||||
decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
|
||||
val size = info.size
|
||||
val maxDimension = maxOf(size.width, size.height)
|
||||
if (maxDimension > MAX_IMAGE_SIZE * 2) {
|
||||
val ratio = (MAX_IMAGE_SIZE * 2).toFloat() / maxDimension.toFloat()
|
||||
val targetW = (size.width * ratio).toInt().coerceAtLeast(1)
|
||||
val targetH = (size.height * ratio).toInt().coerceAtLeast(1)
|
||||
decoder.setTargetSize(targetW, targetH)
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
} catch (_: OutOfMemoryError) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user