From d4104e2ed28ead4617725afc77a5ae5a26b7b095 Mon Sep 17 00:00:00 2001 From: Shihaam Abdul Rahman Date: Sat, 16 May 2026 21:42:04 +0500 Subject: [PATCH] add support for fahipay images --- .../basedbank/api/fahipay/FahipayLoginFlow.kt | 3 +- .../sh/sar/basedbank/api/mib/MibModels.kt | 3 +- .../sar/basedbank/api/mib/TransactionCache.kt | 4 ++- .../ui/home/AccountHistoryAdapter.kt | 18 ++++++++++-- .../ui/home/AccountHistoryFragment.kt | 28 +++++++++++++++++++ .../sar/basedbank/util/MerchantIconCache.kt | 24 ++++++++++++++++ 6 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/sh/sar/basedbank/util/MerchantIconCache.kt diff --git a/app/src/main/java/sh/sar/basedbank/api/fahipay/FahipayLoginFlow.kt b/app/src/main/java/sh/sar/basedbank/api/fahipay/FahipayLoginFlow.kt index d53fec0..ad8482c 100644 --- a/app/src/main/java/sh/sar/basedbank/api/fahipay/FahipayLoginFlow.kt +++ b/app/src/main/java/sh/sar/basedbank/api/fahipay/FahipayLoginFlow.kt @@ -237,7 +237,8 @@ class FahipayLoginFlow { reference = e.optString("transaction").takeIf { it.isNotBlank() }, accountNumber = accountNumber, accountDisplayName = accountDisplayName, - source = "FAHIPAY" + source = "FAHIPAY", + iconUrl = e.optString("icon").takeIf { it.isNotBlank() } ) } Pair(list, total) diff --git a/app/src/main/java/sh/sar/basedbank/api/mib/MibModels.kt b/app/src/main/java/sh/sar/basedbank/api/mib/MibModels.kt index bc97f5a..ba62d33 100644 --- a/app/src/main/java/sh/sar/basedbank/api/mib/MibModels.kt +++ b/app/src/main/java/sh/sar/basedbank/api/mib/MibModels.kt @@ -82,7 +82,8 @@ data class Transaction( val reference: String?, val accountNumber: String, val accountDisplayName: String, - val source: String // "MIB", "BML", "BML_CARD" + val source: String, // "MIB", "BML", "BML_CARD", "FAHIPAY" + val iconUrl: String? = null // merchant icon URL (Fahipay only) ) data class MibFinanceDeal( diff --git a/app/src/main/java/sh/sar/basedbank/api/mib/TransactionCache.kt b/app/src/main/java/sh/sar/basedbank/api/mib/TransactionCache.kt index 4b3d8ba..fb9f7ee 100644 --- a/app/src/main/java/sh/sar/basedbank/api/mib/TransactionCache.kt +++ b/app/src/main/java/sh/sar/basedbank/api/mib/TransactionCache.kt @@ -22,6 +22,7 @@ object TransactionCache { put("accountNumber", t.accountNumber) put("accountDisplayName", t.accountDisplayName) put("source", t.source) + if (t.iconUrl != null) put("iconUrl", t.iconUrl) }) File(context.cacheDir, "tx_$key.json").writeText(CacheEncryption.encrypt(arr.toString())) } catch (_: Exception) {} @@ -46,7 +47,8 @@ object TransactionCache { reference = o.optString("reference").takeIf { it.isNotBlank() }, accountNumber = o.optString("accountNumber"), accountDisplayName = o.optString("accountDisplayName"), - source = o.optString("source") + source = o.optString("source"), + iconUrl = o.optString("iconUrl").takeIf { it.isNotBlank() } ) } } catch (_: Exception) { emptyList() } 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 74db563..5f22493 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 @@ -31,7 +31,9 @@ class AccountHistoryAdapter( private val displayItems = mutableListOf() private var lastInsertedDateKey = "" private val imageCache = mutableMapOf() + private val iconUrlCache = mutableMapOf() var onImageNeeded: ((counterpartyName: String) -> Unit)? = null + var onIconUrlNeeded: ((url: String) -> Unit)? = null fun updateImage(counterpartyName: String, bitmap: Bitmap) { imageCache[counterpartyName] = bitmap @@ -41,6 +43,14 @@ class AccountHistoryAdapter( } } + fun updateIconUrl(url: String, bitmap: Bitmap) { + iconUrlCache[url] = bitmap + displayItems.forEachIndexed { i, item -> + if (item is Item.Trx && item.transaction.iconUrl == url) + notifyItemChanged(i + 1) // +1 for account header at position 0 + } + } + private var _showLoadingFooter = false var showLoadingFooter: Boolean get() = _showLoadingFooter @@ -182,7 +192,10 @@ class AccountHistoryAdapter( val name = trx.counterpartyName ?: trx.description val initial = name.firstOrNull()?.uppercaseChar()?.toString() ?: "?" - val bitmap = trx.counterpartyName?.let { imageCache[it] } + val iconBitmap = trx.iconUrl?.let { iconUrlCache[it] } + val contactBitmap = if (iconBitmap == null) trx.counterpartyName?.let { imageCache[it] } else null + val bitmap = iconBitmap ?: contactBitmap + if (bitmap != null) { val rd = RoundedBitmapDrawableFactory.create(b.root.resources, bitmap) rd.isCircular = true @@ -196,7 +209,8 @@ class AccountHistoryAdapter( b.fvAvatar.background = circle b.tvInitial.visibility = View.VISIBLE b.tvInitial.text = initial - if (trx.counterpartyName != null) onImageNeeded?.invoke(trx.counterpartyName) + if (trx.iconUrl != null) onIconUrlNeeded?.invoke(trx.iconUrl) + else if (trx.counterpartyName != null) onImageNeeded?.invoke(trx.counterpartyName) } b.tvDescription.text = trx.description 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 80324b4..dd4611e 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,6 +1,8 @@ package sh.sar.basedbank.ui.home import android.graphics.BitmapFactory +import okhttp3.OkHttpClient +import okhttp3.Request import android.os.Bundle import android.text.Editable import android.text.TextWatcher @@ -27,6 +29,7 @@ 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 sh.sar.basedbank.util.MerchantIconCache import java.text.SimpleDateFormat import java.util.Calendar import java.util.Locale @@ -44,6 +47,7 @@ class AccountHistoryFragment : Fragment() { private var searchQuery = "" private var firstPageDone = false private val pendingImageNames = mutableSetOf() + private val pendingIconUrls = mutableSetOf() // Pagination state private var mibNextStart = 1 @@ -77,6 +81,7 @@ class AccountHistoryFragment : Fragment() { adapter = AccountHistoryAdapter(account) adapter.onImageNeeded = { name -> loadContactImage(name) } + adapter.onIconUrlNeeded = { url -> loadMerchantIcon(url) } binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) binding.recyclerView.adapter = adapter binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { @@ -266,6 +271,29 @@ class AccountHistoryFragment : Fragment() { } } + private fun loadMerchantIcon(url: String) { + if (!pendingIconUrls.add(url)) return + val cached = MerchantIconCache.load(requireContext(), url) + if (cached != null) { + binding.recyclerView.post { adapter.updateIconUrl(url, cached) } + return + } + viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) { + try { + val client = OkHttpClient() + val response = client.newCall(Request.Builder().url(url).build()).execute() + val bytes = response.body?.bytes() ?: return@launch + response.close() + val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) + ?: return@launch + MerchantIconCache.save(requireContext(), url, bitmap) + withContext(Dispatchers.Main) { adapter.updateIconUrl(url, bitmap) } + } catch (_: Exception) { + pendingIconUrls.remove(url) + } + } + } + override fun onDestroyView() { (activity as? HomeActivity)?.setRefreshing(false) super.onDestroyView() diff --git a/app/src/main/java/sh/sar/basedbank/util/MerchantIconCache.kt b/app/src/main/java/sh/sar/basedbank/util/MerchantIconCache.kt new file mode 100644 index 0000000..34a2acf --- /dev/null +++ b/app/src/main/java/sh/sar/basedbank/util/MerchantIconCache.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 MerchantIconCache { + + private fun file(context: Context, url: String) = + File(context.cacheDir, "micon_${url.replace(Regex("[^A-Za-z0-9]"), "_")}.png") + + fun save(context: Context, url: String, bitmap: Bitmap) { + try { + file(context, url).outputStream().use { + bitmap.compress(Bitmap.CompressFormat.PNG, 90, it) + } + } catch (_: Exception) {} + } + + fun load(context: Context, url: String): Bitmap? = try { + BitmapFactory.decodeFile(file(context, url).absolutePath) + } catch (_: Exception) { null } +}