From c9ec43de0495cf91e8fda70a6b0f7af92e328a3e Mon Sep 17 00:00:00 2001 From: Shihaam Abdul Rahman Date: Fri, 15 May 2026 10:12:09 +0500 Subject: [PATCH] fix scroll bug --- .../java/sh/sar/basedbank/BasedBankApp.kt | 4 ++ .../sh/sar/basedbank/api/mib/MibLoginFlow.kt | 5 +- .../ui/home/AccountHistoryAdapter.kt | 65 ++++++++++++++--- .../ui/home/AccountHistoryFragment.kt | 69 +++++++++++++++---- .../sar/basedbank/ui/home/ContactsFragment.kt | 8 +++ .../basedbank/ui/home/TransactionAdapter.kt | 42 ++++++++--- .../ui/home/TransferHistoryFragment.kt | 67 +++++++++++++----- .../sar/basedbank/util/ContactImageCache.kt | 24 +++++++ 8 files changed, 232 insertions(+), 52 deletions(-) create mode 100644 app/src/main/java/sh/sar/basedbank/util/ContactImageCache.kt diff --git a/app/src/main/java/sh/sar/basedbank/BasedBankApp.kt b/app/src/main/java/sh/sar/basedbank/BasedBankApp.kt index df04edf..bbfe78e 100644 --- a/app/src/main/java/sh/sar/basedbank/BasedBankApp.kt +++ b/app/src/main/java/sh/sar/basedbank/BasedBankApp.kt @@ -3,6 +3,7 @@ package sh.sar.basedbank import android.app.Application import androidx.appcompat.app.AppCompatDelegate import com.google.android.material.color.DynamicColors +import kotlinx.coroutines.sync.Mutex import sh.sar.basedbank.api.bml.BmlSession import sh.sar.basedbank.api.mib.MibAccount import sh.sar.basedbank.api.mib.MibLoginFlow @@ -19,6 +20,9 @@ class BasedBankApp : Application() { var bmlSession: BmlSession? = null var bmlAccounts: List = emptyList() + /** Serialises all MIB profile-switch + request sequences to prevent session corruption. */ + val mibMutex = Mutex() + val mibLoginFlow by lazy { MibLoginFlow(getSharedPreferences("mib_prefs", MODE_PRIVATE)) } diff --git a/app/src/main/java/sh/sar/basedbank/api/mib/MibLoginFlow.kt b/app/src/main/java/sh/sar/basedbank/api/mib/MibLoginFlow.kt index 96dc130..e5a5fc5 100644 --- a/app/src/main/java/sh/sar/basedbank/api/mib/MibLoginFlow.kt +++ b/app/src/main/java/sh/sar/basedbank/api/mib/MibLoginFlow.kt @@ -180,8 +180,9 @@ class MibLoginFlow(private val prefs: android.content.SharedPreferences) { .add("data", encrypted) .build() val response = post(formBody) - val result = MibCrypto.decrypt(response, session.sessionKey) - return result + // Server returns plain JSON (not encrypted) for error responses (e.g. expired session) + if (response.trimStart().startsWith("{")) return JSONObject(response) + return MibCrypto.decrypt(response, session.sessionKey) } private fun baseData(session: MibSession, routePath: String): JSONObject = JSONObject().apply { diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/AccountHistoryAdapter.kt b/app/src/main/java/sh/sar/basedbank/ui/home/AccountHistoryAdapter.kt index adcbf50..08660af 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/AccountHistoryAdapter.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/AccountHistoryAdapter.kt @@ -1,7 +1,9 @@ package sh.sar.basedbank.ui.home +import android.graphics.Bitmap import android.graphics.Color import android.graphics.drawable.GradientDrawable +import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -27,6 +29,17 @@ class AccountHistoryAdapter( } private val displayItems = mutableListOf() + private var lastInsertedDateKey = "" + private val imageCache = mutableMapOf() + var onImageNeeded: ((counterpartyName: String) -> Unit)? = null + + fun updateImage(counterpartyName: String, bitmap: Bitmap) { + imageCache[counterpartyName] = bitmap + displayItems.forEachIndexed { i, item -> + if (item is Item.Trx && item.transaction.counterpartyName == counterpartyName) + notifyItemChanged(i + 1) // +1 for account header at position 0 + } + } private var _showLoadingFooter = false var showLoadingFooter: Boolean @@ -43,13 +56,14 @@ class AccountHistoryAdapter( } } - /** +/** * Display the given (already sorted + filtered) list with date group headers. * Silently resets the loading footer so notifyDataSetChanged covers everything. */ fun setTransactions(transactions: List) { _showLoadingFooter = false displayItems.clear() + lastInsertedDateKey = "" var lastDateKey = "" for (trx in transactions) { val dateKey = trx.date.take(10) @@ -59,9 +73,34 @@ class AccountHistoryAdapter( } displayItems.add(Item.Trx(trx)) } + lastInsertedDateKey = lastDateKey notifyDataSetChanged() } + /** + * Appends [newTransactions] (assumed to be older than all existing items) using incremental + * notifications, so the RecyclerView doesn't reset scroll position. + */ + fun appendTransactions(newTransactions: List) { + if (newTransactions.isEmpty()) return + if (_showLoadingFooter) { + val pos = itemCount - 1 + _showLoadingFooter = false + notifyItemRemoved(pos) + } + val oldCount = displayItems.size + for (trx in newTransactions) { + val dateKey = trx.date.take(10) + if (dateKey != lastInsertedDateKey) { + displayItems.add(Item.DateHeader(formatDateHeader(trx.date))) + lastInsertedDateKey = dateKey + } + displayItems.add(Item.Trx(trx)) + } + val added = displayItems.size - oldCount + if (added > 0) notifyItemRangeInserted(1 + oldCount, added) // +1 for account header + } + // Position 0 = account header card // Positions 1..displayItems.size = date headers + transactions // Last position = loading footer when showLoadingFooter = true @@ -136,15 +175,25 @@ class AccountHistoryAdapter( fun bind(trx: Transaction) { val isCredit = trx.amount >= 0 val color = sourceColor(trx.source) - val initial = (trx.counterpartyName ?: trx.description) - .firstOrNull()?.uppercaseChar()?.toString() ?: "?" + val name = trx.counterpartyName ?: trx.description + val initial = name.firstOrNull()?.uppercaseChar()?.toString() ?: "?" - val circle = GradientDrawable().apply { - shape = GradientDrawable.OVAL - setColor(Color.parseColor(color)) + val bitmap = trx.counterpartyName?.let { imageCache[it] } + if (bitmap != null) { + val rd = RoundedBitmapDrawableFactory.create(b.root.resources, bitmap) + rd.isCircular = true + b.fvAvatar.background = rd + b.tvInitial.visibility = View.INVISIBLE + } else { + val circle = GradientDrawable().apply { + shape = GradientDrawable.OVAL + setColor(Color.parseColor(color)) + } + b.fvAvatar.background = circle + b.tvInitial.visibility = View.VISIBLE + b.tvInitial.text = initial + if (trx.counterpartyName != null) onImageNeeded?.invoke(trx.counterpartyName) } - b.fvAvatar.background = circle - b.tvInitial.text = initial b.tvDescription.text = trx.description val counterparty = trx.counterpartyName diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/AccountHistoryFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/AccountHistoryFragment.kt index a99f1b9..3108fe5 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/AccountHistoryFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/AccountHistoryFragment.kt @@ -1,8 +1,10 @@ package sh.sar.basedbank.ui.home +import android.graphics.BitmapFactory import android.os.Bundle import android.text.Editable import android.text.TextWatcher +import android.util.Base64 import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -13,14 +15,17 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import sh.sar.basedbank.BasedBankApp import sh.sar.basedbank.api.bml.BmlLoginFlow import sh.sar.basedbank.api.mib.MibAccount +import sh.sar.basedbank.api.mib.MibContactsClient import sh.sar.basedbank.api.mib.MibHistoryClient import sh.sar.basedbank.api.mib.Transaction import sh.sar.basedbank.api.mib.TransactionCache import sh.sar.basedbank.databinding.FragmentAccountHistoryBinding +import sh.sar.basedbank.util.ContactImageCache import java.text.SimpleDateFormat import java.util.Calendar import java.util.Locale @@ -37,6 +42,7 @@ class AccountHistoryFragment : Fragment() { private val allTransactions = mutableListOf() private var searchQuery = "" private var firstPageDone = false + private val pendingImageNames = mutableSetOf() // Pagination state private var mibNextStart = 1 @@ -67,6 +73,7 @@ class AccountHistoryFragment : Fragment() { account = viewModel.accounts.value?.find { it.accountNumber == accountNumber } ?: return adapter = AccountHistoryAdapter(account) + adapter.onImageNeeded = { name -> loadContactImage(name) } binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) binding.recyclerView.adapter = adapter binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { @@ -74,7 +81,7 @@ class AccountHistoryFragment : Fragment() { if (dy <= 0 || isLoading) return val lm = rv.layoutManager as LinearLayoutManager if (lm.findLastVisibleItemPosition() >= adapter.itemCount - 3) { - loadNextPage() + rv.post { loadNextPage() } } } }) @@ -138,19 +145,20 @@ class AccountHistoryFragment : Fragment() { when { isMib() -> { val session = app.mibSession ?: return@withContext emptyList() - val profile = app.mibProfiles.firstOrNull { it.profileId == account.profileId } - if (profile != null) app.mibLoginFlow.switchProfile(session, profile) - val client = MibHistoryClient() - val (list, total) = client.fetchHistory( - session = session, - accountNo = account.accountNumber, - accountDisplayName = account.accountBriefName, - start = mibNextStart, - pageSize = pageSize - ) - if (total > 0) mibTotalCount = total - mibNextStart += list.size.coerceAtLeast(pageSize) - list + app.mibMutex.withLock { + val profile = app.mibProfiles.firstOrNull { it.profileId == account.profileId } + if (profile != null) app.mibLoginFlow.switchProfile(session, profile) + val (list, total) = MibHistoryClient().fetchHistory( + session = session, + accountNo = account.accountNumber, + accountDisplayName = account.accountBriefName, + start = mibNextStart, + pageSize = pageSize + ) + if (total > 0) mibTotalCount = total + mibNextStart += list.size.coerceAtLeast(pageSize) + list + } } isBmlCard() -> { val session = app.bmlSession ?: return@withContext emptyList() @@ -196,7 +204,14 @@ class AccountHistoryFragment : Fragment() { allTransactions.addAll(newOnes) allTransactions.sortByDescending { AccountHistoryAdapter.parseDateMillis(it.date) } TransactionCache.save(requireContext(), account.accountNumber, allTransactions) - filterAndDisplay() + if (searchQuery.isBlank()) { + // Append incrementally to preserve scroll position + val sorted = newOnes.sortedByDescending { AccountHistoryAdapter.parseDateMillis(it.date) } + adapter.appendTransactions(sorted) + binding.emptyView.visibility = View.GONE + } else { + filterAndDisplay() + } } else { adapter.showLoadingFooter = false } @@ -208,6 +223,30 @@ class AccountHistoryFragment : Fragment() { } } + private fun loadContactImage(name: String) { + if (!pendingImageNames.add(name)) return + val contact = viewModel.contacts.value?.firstOrNull { it.benefNickName == name } ?: return + val hash = contact.customerImgHash ?: return + val cached = ContactImageCache.load(requireContext(), hash) + if (cached != null) { + binding.recyclerView.post { adapter.updateImage(name, cached) } + return + } + val app = requireActivity().application as BasedBankApp + val sess = app.mibSession ?: return + viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) { + try { + val base64 = MibContactsClient().fetchProfileImageBase64(sess, hash) ?: return@launch + val bytes = Base64.decode(base64, Base64.DEFAULT) + val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) ?: return@launch + ContactImageCache.save(requireContext(), hash, bitmap) + withContext(Dispatchers.Main) { adapter.updateImage(name, bitmap) } + } catch (_: Exception) { + pendingImageNames.remove(name) + } + } + } + override fun onDestroyView() { (activity as? HomeActivity)?.setRefreshing(false) super.onDestroyView() diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/ContactsFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/ContactsFragment.kt index 9d0b671..bb9b163 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/ContactsFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/ContactsFragment.kt @@ -26,6 +26,7 @@ import sh.sar.basedbank.api.mib.MibBeneficiary import sh.sar.basedbank.api.mib.MibBeneficiaryCategory import sh.sar.basedbank.api.mib.MibContactsClient import sh.sar.basedbank.databinding.FragmentContactsBinding +import sh.sar.basedbank.util.ContactImageCache import sh.sar.basedbank.util.ContactsCache class ContactsFragment : Fragment() { @@ -212,6 +213,12 @@ class ContactsFragment : Fragment() { private fun fetchImage(hash: String) { if (!pendingHashes.add(hash)) return + // Check disk cache first — if hash matches we already have the image + val cached = ContactImageCache.load(requireContext(), hash) + if (cached != null) { + view?.post { pagerAdapter.updateImage(hash, cached) } + return + } val sess = session ?: return val client = MibContactsClient() viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) { @@ -219,6 +226,7 @@ class ContactsFragment : Fragment() { val base64 = client.fetchProfileImageBase64(sess, hash) ?: return@launch val bytes = Base64.decode(base64, Base64.DEFAULT) val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) ?: return@launch + ContactImageCache.save(requireContext(), hash, bitmap) withContext(Dispatchers.Main) { pagerAdapter.updateImage(hash, bitmap) } } catch (_: Exception) { pendingHashes.remove(hash) diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/TransactionAdapter.kt b/app/src/main/java/sh/sar/basedbank/ui/home/TransactionAdapter.kt index e7ecf16..2d5b9ad 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/TransactionAdapter.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/TransactionAdapter.kt @@ -1,10 +1,12 @@ package sh.sar.basedbank.ui.home +import android.graphics.Bitmap import android.graphics.Color import android.graphics.drawable.GradientDrawable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory import androidx.recyclerview.widget.RecyclerView import com.google.android.material.dialog.MaterialAlertDialogBuilder import sh.sar.basedbank.api.mib.Transaction @@ -21,6 +23,16 @@ class TransactionAdapter : RecyclerView.Adapter() { } private val displayItems = mutableListOf() + private val imageCache = mutableMapOf() + var onImageNeeded: ((counterpartyName: String) -> Unit)? = null + + fun updateImage(counterpartyName: String, bitmap: Bitmap) { + imageCache[counterpartyName] = bitmap + displayItems.forEachIndexed { i, item -> + if (item is Item.Trx && item.transaction.counterpartyName == counterpartyName) + notifyItemChanged(i) + } + } private var _showLoadingFooter = false var showLoadingFooter: Boolean @@ -39,7 +51,7 @@ class TransactionAdapter : RecyclerView.Adapter() { /** Replace the full sorted transaction list and rebuild date groups. */ fun setTransactions(transactions: List) { - _showLoadingFooter = false // reset silently; notifyDataSetChanged covers it + _showLoadingFooter = false displayItems.clear() var lastDateKey = "" for (trx in transactions) { @@ -53,10 +65,10 @@ class TransactionAdapter : RecyclerView.Adapter() { notifyDataSetChanged() } - override fun getItemCount() = displayItems.size + if (showLoadingFooter) 1 else 0 + override fun getItemCount() = displayItems.size + if (_showLoadingFooter) 1 else 0 override fun getItemViewType(position: Int) = when { - showLoadingFooter && position == displayItems.size -> TYPE_LOADING + _showLoadingFooter && position == displayItems.size -> TYPE_LOADING displayItems[position] is Item.DateHeader -> TYPE_DATE_HEADER else -> TYPE_TRANSACTION } @@ -88,15 +100,25 @@ class TransactionAdapter : RecyclerView.Adapter() { fun bind(trx: Transaction) { val isCredit = trx.amount >= 0 val color = AccountHistoryAdapter.sourceColor(trx.source) - val initial = (trx.counterpartyName ?: trx.description) - .firstOrNull()?.uppercaseChar()?.toString() ?: "?" + val name = trx.counterpartyName ?: trx.description + val initial = name.firstOrNull()?.uppercaseChar()?.toString() ?: "?" - val circle = GradientDrawable().apply { - shape = GradientDrawable.OVAL - setColor(Color.parseColor(color)) + val bitmap = trx.counterpartyName?.let { imageCache[it] } + if (bitmap != null) { + val rd = RoundedBitmapDrawableFactory.create(b.root.resources, bitmap) + rd.isCircular = true + b.fvAvatar.background = rd + b.tvInitial.visibility = View.INVISIBLE + } else { + val circle = GradientDrawable().apply { + shape = GradientDrawable.OVAL + setColor(Color.parseColor(color)) + } + b.fvAvatar.background = circle + b.tvInitial.visibility = View.VISIBLE + b.tvInitial.text = initial + if (trx.counterpartyName != null) onImageNeeded?.invoke(trx.counterpartyName) } - b.fvAvatar.background = circle - b.tvInitial.text = initial b.tvDescription.text = trx.description // Show account name in secondary line for Transfer History diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/TransferHistoryFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/TransferHistoryFragment.kt index f2dc932..0212fb0 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/TransferHistoryFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/TransferHistoryFragment.kt @@ -1,8 +1,10 @@ package sh.sar.basedbank.ui.home +import android.graphics.BitmapFactory import android.os.Bundle import android.text.Editable import android.text.TextWatcher +import android.util.Base64 import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -15,15 +17,18 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import sh.sar.basedbank.BasedBankApp import sh.sar.basedbank.R import sh.sar.basedbank.api.bml.BmlLoginFlow import sh.sar.basedbank.api.mib.MibAccount +import sh.sar.basedbank.api.mib.MibContactsClient import sh.sar.basedbank.api.mib.MibHistoryClient import sh.sar.basedbank.api.mib.Transaction import sh.sar.basedbank.api.mib.TransactionCache import sh.sar.basedbank.databinding.FragmentTransferHistoryBinding +import sh.sar.basedbank.util.ContactImageCache import java.text.SimpleDateFormat import java.util.Calendar import java.util.Locale @@ -60,6 +65,7 @@ class TransferHistoryFragment : Fragment() { private var isLoading = false private var firstBatchDone = false private val pageSize = 20 + private val pendingImageNames = mutableSetOf() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { _binding = FragmentTransferHistoryBinding.inflate(inflater, container, false) @@ -68,6 +74,7 @@ class TransferHistoryFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { adapter = TransactionAdapter() + adapter.onImageNeeded = { name -> loadContactImage(name) } binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) binding.recyclerView.adapter = adapter @@ -76,7 +83,7 @@ class TransferHistoryFragment : Fragment() { if (dy <= 0 || isLoading) return val lm = rv.layoutManager as LinearLayoutManager if (lm.findLastVisibleItemPosition() >= adapter.itemCount - 5) { - loadNextPages() + rv.post { loadNextPages() } } } }) @@ -171,25 +178,27 @@ class TransferHistoryFragment : Fragment() { } }.awaitAll().flatten()) - // MIB accounts: serialized per profile to avoid session race + // MIB accounts: serialized per profile, protected by mutex to prevent session race val mibStates = activeStates.filter { !it.account.profileType.startsWith("BML") } for ((profileId, states) in mibStates.groupBy { it.account.profileId }) { val session = mibSession ?: break - val profile = app.mibProfiles.firstOrNull { it.profileId == profileId } - if (profile != null) app.mibLoginFlow.switchProfile(session, profile) - for (state in states) { - try { - val (list, total) = MibHistoryClient().fetchHistory( - session = session, - accountNo = state.account.accountNumber, - accountDisplayName = state.account.accountBriefName, - start = state.mibNextStart, - pageSize = pageSize - ) - if (total > 0) state.mibTotalCount = total - state.mibNextStart += list.size.coerceAtLeast(pageSize) - results.addAll(list) - } catch (_: Exception) {} + app.mibMutex.withLock { + val profile = app.mibProfiles.firstOrNull { it.profileId == profileId } + if (profile != null) app.mibLoginFlow.switchProfile(session, profile) + for (state in states) { + try { + val (list, total) = MibHistoryClient().fetchHistory( + session = session, + accountNo = state.account.accountNumber, + accountDisplayName = state.account.accountBriefName, + start = state.mibNextStart, + pageSize = pageSize + ) + if (total > 0) state.mibTotalCount = total + state.mibNextStart += list.size.coerceAtLeast(pageSize) + results.addAll(list) + } catch (_: Exception) {} + } } } @@ -232,6 +241,30 @@ class TransferHistoryFragment : Fragment() { binding.emptyView.visibility = if (filtered.isEmpty() && !isLoading) View.VISIBLE else View.GONE } + private fun loadContactImage(name: String) { + if (!pendingImageNames.add(name)) return + val contact = viewModel.contacts.value?.firstOrNull { it.benefNickName == name } ?: return + val hash = contact.customerImgHash ?: return + val cached = ContactImageCache.load(requireContext(), hash) + if (cached != null) { + binding.recyclerView.post { adapter.updateImage(name, cached) } + return + } + val app = requireActivity().application as BasedBankApp + val sess = app.mibSession ?: return + viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) { + try { + val base64 = MibContactsClient().fetchProfileImageBase64(sess, hash) ?: return@launch + val bytes = Base64.decode(base64, Base64.DEFAULT) + val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) ?: return@launch + ContactImageCache.save(requireContext(), hash, bitmap) + withContext(Dispatchers.Main) { adapter.updateImage(name, bitmap) } + } catch (_: Exception) { + pendingImageNames.remove(name) + } + } + } + override fun onDestroyView() { (activity as? HomeActivity)?.setRefreshing(false) super.onDestroyView() diff --git a/app/src/main/java/sh/sar/basedbank/util/ContactImageCache.kt b/app/src/main/java/sh/sar/basedbank/util/ContactImageCache.kt new file mode 100644 index 0000000..f96019c --- /dev/null +++ b/app/src/main/java/sh/sar/basedbank/util/ContactImageCache.kt @@ -0,0 +1,24 @@ +package sh.sar.basedbank.util + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import java.io.File + +object ContactImageCache { + + private fun file(context: Context, hash: String) = + File(context.cacheDir, "cimg_${hash.replace(Regex("[^A-Za-z0-9]"), "_")}.png") + + fun save(context: Context, hash: String, bitmap: Bitmap) { + try { + file(context, hash).outputStream().use { + bitmap.compress(Bitmap.CompressFormat.PNG, 90, it) + } + } catch (_: Exception) {} + } + + fun load(context: Context, hash: String): Bitmap? = try { + BitmapFactory.decodeFile(file(context, hash).absolutePath) + } catch (_: Exception) { null } +}