Optimized decoding of bitmaps

This commit is contained in:
Juha Heinanen
2026-07-07 15:55:11 +03:00
parent 0e3e587991
commit 8944f3769d
4 changed files with 64 additions and 9 deletions

View File

@ -3,7 +3,6 @@ package com.tutpro.baresip
import android.content.Context import android.content.Context
import android.database.Cursor import android.database.Cursor
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.provider.ContactsContract import android.provider.ContactsContract
import androidx.core.net.toUri import androidx.core.net.toUri
@ -302,7 +301,7 @@ sealed class Contact {
val avatarFilePath = BaresipService.filesPath + "/$id.png" val avatarFilePath = BaresipService.filesPath + "/$id.png"
if (File(avatarFilePath).exists()) { if (File(avatarFilePath).exists()) {
try { try {
contact.avatarImage = BitmapFactory.decodeFile(avatarFilePath) contact.avatarImage = Utils.decodeSampledBitmapFromFile(avatarFilePath, 192, 192)
Log.d(TAG, "Set avatarImage") Log.d(TAG, "Set avatarImage")
if (contact.avatarImage == null) if (contact.avatarImage == null)
Log.d(TAG, "Contact $id avatarImage is null") Log.d(TAG, "Contact $id avatarImage is null")

View File

@ -468,9 +468,7 @@ private fun AvatarSection(
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? -> rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
if (uri != null) { if (uri != null) {
try { try {
val inputStream = ctx.contentResolver.openInputStream(uri) val avatarBitmap = Utils.decodeSampledBitmapFromUri(ctx, uri, 192, 192)
val avatarBitmap = BitmapFactory.decodeStream(inputStream)
inputStream?.close()
if (avatarBitmap == null) { if (avatarBitmap == null) {
Log.e(TAG, "Failed to decode bitmap from URI: $uri") Log.e(TAG, "Failed to decode bitmap from URI: $uri")

View File

@ -5,7 +5,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import androidx.core.graphics.scale
import android.provider.ContactsContract import android.provider.ContactsContract
import android.util.Base64 import android.util.Base64
import android.util.Log import android.util.Log
@ -243,11 +243,17 @@ private fun ContactsScreen(navController: NavController) {
if (photoBase64.isNotEmpty()) if (photoBase64.isNotEmpty())
try { try {
val decodedString = Base64.decode(photoBase64, Base64.DEFAULT) val decodedString = Base64.decode(photoBase64, Base64.DEFAULT)
val decodedByte = BitmapFactory.decodeByteArray(decodedString, 0, decodedString.size) val decodedByte = Utils.decodeSampledBitmapFromByteArray(decodedString, 192, 192)
if (decodedByte != null) { if (decodedByte != null) {
val scaledByte = decodedByte.scale(192, 192)
val avatarFile = File(BaresipService.filesPath, "$contactId.png") val avatarFile = File(BaresipService.filesPath, "$contactId.png")
FileOutputStream(avatarFile).use { out -> try {
decodedByte.compress(Bitmap.CompressFormat.PNG, 100, out) val out = FileOutputStream(avatarFile)
scaledByte.compress(Bitmap.CompressFormat.PNG, 100, out)
out.flush()
out.close()
} catch (e: Exception) {
Log.e(TAG, "Failed to save photo for $name: ${e.message}")
} }
} }
} catch (e: Exception) { } catch (e: Exception) {

View File

@ -15,6 +15,7 @@ import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Color import android.graphics.Color
import android.graphics.PorterDuff import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode import android.graphics.PorterDuffXfermode
@ -1390,6 +1391,57 @@ object Utils {
} }
} }
fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
val (height: Int, width: Int) = options.outHeight to options.outWidth
var inSampleSize = 1
if (height > reqHeight || width > reqWidth) {
val halfHeight = height / 2
val halfWidth = width / 2
while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth)
inSampleSize *= 2
}
return inSampleSize
}
fun decodeSampledBitmapFromUri(ctx: Context, uri: Uri, reqWidth: Int, reqHeight: Int): Bitmap? {
return try {
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
ctx.contentResolver.openInputStream(uri).use { BitmapFactory.decodeStream(it, null, options) }
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight)
options.inJustDecodeBounds = false
ctx.contentResolver.openInputStream(uri).use { BitmapFactory.decodeStream(it, null, options) }
} catch (e: Exception) {
Log.e(TAG, "Failed to decode sampled bitmap from URI: ${e.message}")
null
}
}
fun decodeSampledBitmapFromByteArray(data: ByteArray, reqWidth: Int, reqHeight: Int): Bitmap? {
return try {
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeByteArray(data, 0, data.size, options)
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight)
options.inJustDecodeBounds = false
BitmapFactory.decodeByteArray(data, 0, data.size, options)
} catch (e: Exception) {
Log.e(TAG, "Failed to decode sampled bitmap from ByteArray: ${e.message}")
null
}
}
fun decodeSampledBitmapFromFile(path: String, reqWidth: Int, reqHeight: Int): Bitmap? {
return try {
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeFile(path, options)
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight)
options.inJustDecodeBounds = false
BitmapFactory.decodeFile(path, options)
} catch (e: Exception) {
Log.e(TAG, "Failed to decode sampled bitmap from file: ${e.message}")
null
}
}
@RequiresApi(29) @RequiresApi(29)
@SuppressLint("HardwareIds") @SuppressLint("HardwareIds")
fun getLine1Number(ctx: Context, subscriptionId: Int = SubscriptionManager.DEFAULT_SUBSCRIPTION_ID): String? { fun getLine1Number(ctx: Context, subscriptionId: Int = SubscriptionManager.DEFAULT_SUBSCRIPTION_ID): String? {