diff --git a/jami-android/app/src/main/java/cx/ring/services/ContactServiceImpl.kt b/jami-android/app/src/main/java/cx/ring/services/ContactServiceImpl.kt index 9fed25010aa0e941682ca79ad7885485cf19e56c..c1d93a36c6c211af477ccdba4092694b91f0fa6b 100644 --- a/jami-android/app/src/main/java/cx/ring/services/ContactServiceImpl.kt +++ b/jami-android/app/src/main/java/cx/ring/services/ContactServiceImpl.kt @@ -450,7 +450,7 @@ class ContactServiceImpl(val mContext: Context, preferenceService: PreferencesSe private fun loadVCardContactData(contact: Contact, accountId: String): Single<Profile> = Single.fromCallable { val id = Base64.encodeToString(contact.primaryNumber.toByteArray(), Base64.NO_WRAP) - VCardServiceImpl.readData(VCardUtils.loadPeerProfileFromDisk(mContext.filesDir, mContext.cacheDir, id, accountId)) + VCardServiceImpl.loadPeerProfileFromDisk(mContext.filesDir, mContext.cacheDir, id, accountId) } .subscribeOn(Schedulers.io()) diff --git a/jami-android/app/src/main/java/cx/ring/services/VCardServiceImpl.kt b/jami-android/app/src/main/java/cx/ring/services/VCardServiceImpl.kt index 7f4410f267303e5062efaadd86966dc1f43e1e60..dcee634bd2672ecd8161ad1725b123bca15a2cef 100644 --- a/jami-android/app/src/main/java/cx/ring/services/VCardServiceImpl.kt +++ b/jami-android/app/src/main/java/cx/ring/services/VCardServiceImpl.kt @@ -34,6 +34,7 @@ import io.reactivex.rxjava3.schedulers.Schedulers import net.jami.model.Account import net.jami.model.Profile import java.io.File +import java.io.IOException class VCardServiceImpl(private val mContext: Context) : VCardService() { override fun loadProfile(account: Account): Observable<Profile> = loadProfile(mContext, account) @@ -82,8 +83,7 @@ class VCardServiceImpl(private val mContext: Context) : VCardService() { synchronized(account) { var ret = account.loadedProfile if (ret == null) { - ret = VCardUtils.loadLocalProfileFromDiskWithDefault(context.filesDir, account.accountId) - .map { vcard: VCard -> readData(vcard) } + ret = loadLocalProfileFromDiskWithDefault(context.filesDir, context.cacheDir, account.accountId) .subscribeOn(Schedulers.io()) .cache() account.loadedProfile = ret @@ -100,5 +100,70 @@ class VCardServiceImpl(private val mContext: Context) : VCardService() { fun readData(profile: Pair<String?, ByteArray?>): Profile = Profile(profile.first, BitmapUtils.bytesToBitmap(profile.second)) + + @Throws(IOException::class) + fun loadPeerProfileFromDisk(filesDir: File, cacheDir: File, filename: String, accountId: String): Profile { + val cacheFolder = VCardUtils.peerProfileCachePath(cacheDir, accountId) + val cacheName = File(cacheFolder, "$filename.txt") + val cachePicture = File(cacheFolder, filename) + val profileFile = File(VCardUtils.peerProfilePath(filesDir, accountId), "$filename.vcf") + return loadProfileWithCache(cacheName, cachePicture, profileFile) + } + + @Throws(IOException::class) + fun loadLocalProfileFromDisk(filesDir: File, cacheDir: File, accountId: String): Profile { + val cacheFolder = VCardUtils.localProfileCachePath(cacheDir, accountId) + val cacheName = File(cacheFolder, "${VCardUtils.ACCOUNT_PROFILE_NAME}.txt") + val cachePicture = File(cacheFolder, "${VCardUtils.ACCOUNT_PROFILE_NAME}_pic") + val profileFile = File(VCardUtils.localProfilePath(filesDir, accountId), VCardUtils.LOCAL_USER_VCARD_NAME) + return loadProfileWithCache(cacheName, cachePicture, profileFile) + } + fun loadLocalProfileFromDiskWithDefault(filesDir: File, cacheDir: File, accountId: String): Single<Profile> = + Single.fromCallable { loadLocalProfileFromDisk(filesDir, cacheDir, accountId) } + .onErrorReturn { Profile.EMPTY_PROFILE } + + fun loadProfileWithCache(cacheName: File, cachePicture: File, profileFile: File): Profile { + // Case 1: no profile for this peer + if (!profileFile.exists()) { + return Profile.EMPTY_PROFILE + } + + // Case 2: read profile from cache + if (cacheName.exists() && cacheName.lastModified() >= profileFile.lastModified()) { + return Profile( + cacheName.readText(), + if (cachePicture.exists()) BitmapUtils.bytesToBitmap(cachePicture.readBytes()) else null + ) + } + + // Case 3: read profile from disk and update cache + val (name, picture) = VCardUtils.readData(VCardUtils.loadFromDisk(profileFile)) + cacheName.writeText(name ?: "") + if (picture != null) { + BitmapUtils.bytesToBitmap(picture)?.let { bitmap -> + BitmapUtils.createScaledBitmap(bitmap, 512).apply { + if (this === bitmap) { + // Case 3a: bitmap is already small enough, cache it as-is + Schedulers.io().createWorker().schedule { + cachePicture.outputStream().use { + it.write(picture) + } + } + return Profile(name, bitmap) + } + bitmap.recycle() + } + }?.let { scaledBitmap -> + // Case 3b: bitmap is too big, reduce it and write to cache + Schedulers.io().createWorker().schedule { + cachePicture.outputStream().use { + scaledBitmap.compress(Bitmap.CompressFormat.JPEG, 88, it) + } + } + return Profile(name, scaledBitmap) + } + } + return Profile(name, null) + } } } \ No newline at end of file diff --git a/jami-android/app/src/main/java/cx/ring/utils/BitmapUtils.kt b/jami-android/app/src/main/java/cx/ring/utils/BitmapUtils.kt index 02bd99da143b53a6a17f1d6d5b8a011a007a9c92..781874eb4b7e2bf20606df135f2ccea0c1af3516 100644 --- a/jami-android/app/src/main/java/cx/ring/utils/BitmapUtils.kt +++ b/jami-android/app/src/main/java/cx/ring/utils/BitmapUtils.kt @@ -35,6 +35,8 @@ import ezvcard.property.Photo import net.jami.utils.QRCodeUtils import java.io.ByteArrayOutputStream import java.nio.ByteBuffer +import androidx.core.graphics.scale +import androidx.core.graphics.createBitmap /** * Helper calls to manipulates Bitmaps @@ -103,13 +105,16 @@ object BitmapUtils { while (ratio * ratio < minRatio) ratio *= 2 height /= ratio width /= ratio - val ret = Bitmap.createScaledBitmap(bmp, width, height, true) + val ret = bmp.scale(width, height) Log.d(TAG, "reduceBitmap: bitmap size after x" + ratio + " reduce " + ret.byteCount) return ret } - fun createScaledBitmap(bitmap: Bitmap?, maxSize: Int): Bitmap { - require(!(bitmap == null || maxSize < 0)) + fun createScaledBitmap(bitmap: Bitmap, maxSize: Int): Bitmap { + require(maxSize >= 0) + Log.w(TAG, "createScaledBitmap: ${bitmap.width}x${bitmap.height} -> $maxSize") + if (bitmap.width <= maxSize && bitmap.height <= maxSize) + return bitmap var width = bitmap.height var height = bitmap.width if (width != height) { @@ -126,7 +131,7 @@ object BitmapUtils { width = maxSize height = maxSize } - return Bitmap.createScaledBitmap(bitmap, width, height, true) + return bitmap.scale(width, height) } fun drawableToBitmap(drawable: Drawable, size: Int = -1, padding: Int = 0): Bitmap { @@ -135,8 +140,7 @@ object BitmapUtils { } val width = drawable.intrinsicWidth.takeIf { it > 0 } ?: size val height = drawable.intrinsicHeight.takeIf { it > 0 } ?: size - val bitmap = - Bitmap.createBitmap(width + 2 * padding, height + 2 * padding, Bitmap.Config.ARGB_8888) + val bitmap = createBitmap(width + 2 * padding, height + 2 * padding) val canvas = Canvas(bitmap) drawable.setBounds(padding, padding, canvas.width - padding, canvas.height - padding) drawable.draw(canvas) @@ -162,7 +166,7 @@ object BitmapUtils { private fun pageToBitmap(page: Page, maxWidth: Int, maxHeight: Int): Bitmap = pageRenderSize(page, maxWidth, maxHeight) - .let { (w, h) -> Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888) } + .let { (w, h) -> createBitmap(w, h) } .apply { page.render(this, null, null, Page.RENDER_MODE_FOR_DISPLAY) } fun documentToBitmap(context: Context, uri: Uri, maxWidth: Int = -1, maxHeight: Int = -1): Bitmap? = @@ -187,7 +191,7 @@ object BitmapUtils { drawableToBitmap(drawable, size, size / 5) fun qrToBitmap(qrCodeData: QRCodeUtils.QRCodeData) = - Bitmap.createBitmap(qrCodeData.width, qrCodeData.height, Bitmap.Config.ARGB_8888).apply { + createBitmap(qrCodeData.width, qrCodeData.height).apply { setPixels(qrCodeData.data, 0, qrCodeData.width, 0, 0, qrCodeData.width, qrCodeData.height) } diff --git a/jami-android/libjamiclient/src/main/kotlin/net/jami/utils/VCardUtils.kt b/jami-android/libjamiclient/src/main/kotlin/net/jami/utils/VCardUtils.kt index 8a1db5d9ea7775a03fd9c3a0bb456ddb2ef5c748..69f56965dbe39132185a491f762589d2d5388870 100644 --- a/jami-android/libjamiclient/src/main/kotlin/net/jami/utils/VCardUtils.kt +++ b/jami-android/libjamiclient/src/main/kotlin/net/jami/utils/VCardUtils.kt @@ -16,7 +16,6 @@ */ package net.jami.utils -import net.jami.utils.FileUtils.moveFile import ezvcard.VCard import ezvcard.property.FormattedName import ezvcard.property.Uid @@ -38,7 +37,8 @@ import java.util.HashMap object VCardUtils { val TAG = VCardUtils::class.simpleName!! const val VCARD_KEY_MIME_TYPE = "mimeType" - const val LOCAL_USER_VCARD_NAME = "profile.vcf" + const val ACCOUNT_PROFILE_NAME = "profile" + const val LOCAL_USER_VCARD_NAME = "$ACCOUNT_PROFILE_NAME.vcf" private const val VCARD_MAX_SIZE = 1024L * 1024L * 8 fun readData(vcard: VCard?): Pair<String?, ByteArray?> { @@ -136,36 +136,6 @@ object VCardUtils { else -> "JPEG" } - @Throws(IOException::class) - fun loadPeerProfileFromDisk(filesDir: File, cacheDir: File, filename: String, accountId: String): Pair<String?, ByteArray?> { - val cacheFolder = peerProfileCachePath(cacheDir, accountId) - val cacheName = File(cacheFolder, filename + ".txt") - val cachePicture = File(cacheFolder, filename) - val profileFile = File(peerProfilePath(filesDir, accountId), filename + ".vcf") - - // Case 1: no profile for this peer - if (!profileFile.exists()) { - return Pair(null, null) - } - - // Case 2: read profile from cache - if (cacheName.exists() && cacheName.lastModified() > profileFile.lastModified()) { - return Pair( - cacheName.readText(), - if (cachePicture.exists()) cachePicture.readBytes() else null - ) - } - - // Case 3: read profile from disk and update cache - val data = readData(loadFromDisk(profileFile)) - val (name, picture) = data - cacheName.writeText(name ?: "") - if (picture != null) { - cachePicture.writeBytes(picture) - } - return data - } - fun resetCustomProfileName(accountId: String, filename: String, filesDir: File) { if (filename.isEmpty()) { return @@ -268,18 +238,21 @@ object VCardUtils { return File(accountDir, "CustomPeerProfiles").apply { mkdirs() } } - private fun peerProfilePath(filesDir: File, accountId: String): File { + fun peerProfilePath(filesDir: File, accountId: String): File { val accountDir = File(filesDir, accountId) return File(accountDir, "profiles").apply { mkdirs() } } - private fun peerProfileCachePath(cacheDir: File, accountId: String): File { + fun peerProfileCachePath(cacheDir: File, accountId: String): File { val accountDir = File(cacheDir, accountId) return File(accountDir, "profiles").apply { mkdirs() } } - private fun localProfilePath(filesDir: File, accountId: String): File = + fun localProfilePath(filesDir: File, accountId: String): File = File(filesDir, accountId).apply { mkdir() } + fun localProfileCachePath(cacheDir: File, accountId: String): File = + File(cacheDir, accountId).apply { mkdir() } + private fun defaultProfile(accountId: String): VCard = VCard().apply { Uid(accountId) } fun loadProfile(vcard: File): Single<VCard> =