show bank/profile image in accounts and drop down

This commit is contained in:
2026-05-27 22:00:47 +05:00
parent c98a3e3e89
commit ef919aa179
5 changed files with 114 additions and 12 deletions
@@ -3,11 +3,13 @@ package sh.sar.basedbank.ui.home
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.graphics.Bitmap
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView
import sh.sar.basedbank.R
import sh.sar.basedbank.api.models.BankAccount
import sh.sar.basedbank.databinding.ItemAccountBinding
import sh.sar.basedbank.databinding.ItemCardBinding
@@ -17,7 +19,9 @@ import sh.sar.basedbank.util.AccountListParser
class AccountsAdapter(
accounts: List<BankAccount>,
private val onAccountClick: (BankAccount) -> Unit = {}
private val onAccountClick: (BankAccount) -> Unit = {},
/** Optional loader for MIB per-profile images: (hash, onLoaded) */
private val profileImageLoader: ((String, (Bitmap) -> Unit) -> Unit)? = null
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
var onTransferClick: ((BankAccount) -> Unit)? = null
@@ -112,6 +116,8 @@ class AccountsAdapter(
private inner class AccountViewHolder(private val binding: ItemAccountBinding) :
RecyclerView.ViewHolder(binding.root) {
private var boundHash: String? = null
fun bind(account: BankAccount, display: AccountListDisplay) {
binding.tvAccountName.text = display.name
binding.tvAccountNumber.text = display.number
@@ -123,6 +129,23 @@ class AccountsAdapter(
copyToClipboard(it.context, display.number)
true
}
val staticLogo = when (account.bank) {
"BML" -> R.drawable.bml_logo_vector
"FAHIPAY" -> R.drawable.fahipay_logo
"MIB" -> R.drawable.mib_logo
else -> null
}
if (staticLogo != null) binding.ivBankLogo.setImageResource(staticLogo)
else binding.ivBankLogo.setImageDrawable(null)
val hash = account.profileImageHash
boundHash = hash
if (account.bank == "MIB" && hash != null && profileImageLoader != null) {
profileImageLoader.invoke(hash) { bitmap ->
if (boundHash == hash) binding.ivBankLogo.setImageBitmap(bitmap)
}
}
}
}
@@ -1,6 +1,9 @@
package sh.sar.basedbank.ui.home
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Bundle
import android.util.Base64
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -8,7 +11,12 @@ import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import sh.sar.basedbank.BasedBankApp
import sh.sar.basedbank.R
import sh.sar.basedbank.databinding.FragmentAccountsBinding
@@ -18,6 +26,7 @@ class AccountsFragment : Fragment() {
private val binding get() = _binding!!
private val viewModel: HomeViewModel by activityViewModels()
private lateinit var adapter: AccountsAdapter
private val profileImageCache = mutableMapOf<String, Bitmap>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentAccountsBinding.inflate(inflater, container, false)
@@ -25,9 +34,30 @@ class AccountsFragment : Fragment() {
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
adapter = AccountsAdapter(emptyList()) { account ->
(activity as? HomeActivity)?.showWithBackStack(AccountHistoryFragment.newInstance(account))
}
val app = requireActivity().application as BasedBankApp
adapter = AccountsAdapter(
accounts = emptyList(),
onAccountClick = { account ->
(activity as? HomeActivity)?.showWithBackStack(AccountHistoryFragment.newInstance(account))
},
profileImageLoader = { hash, onLoaded ->
profileImageCache[hash]?.let { onLoaded(it); return@AccountsAdapter }
viewLifecycleOwner.lifecycleScope.launch {
val bitmap = withContext(Dispatchers.IO) {
try {
val session = app.anyMibSession() ?: return@withContext null
val b64 = app.anyMibFlow()?.fetchProfileImage(session, hash) ?: return@withContext null
val bytes = Base64.decode(b64, Base64.DEFAULT)
BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
} catch (_: Exception) { null }
}
if (bitmap != null) {
profileImageCache[hash] = bitmap
onLoaded(bitmap)
}
}
}
)
adapter.onTransferClick = { account ->
(activity as? HomeActivity)?.navigateTo(R.id.nav_transfer, TransferFragment.newInstanceFrom(account))
}
@@ -93,6 +93,7 @@ class TransferFragment : Fragment() {
// BML QR merchant payment mode (set when navigated from a card QR scan)
private var bmlQrInfo: BmlQrPayInfo? = null
private var bmlGatewayQr = false // true for pay.bml.com.mv QRs (requires pre-initiate step)
private val dropdownProfileImageCache = mutableMapOf<String, Bitmap>()
// BML business profile OTP flow state
private enum class BmlOtpState { NONE, SELECTING_CHANNEL, AWAITING_OTP }
@@ -1629,11 +1630,47 @@ class TransferFragment : Fragment() {
b.tvDropdownBalance.text = if (hide && balance.isNotBlank()) maskAmount(balance) else balance
b.root.alpha = if (inactive) 0.4f else 1f
val networkIcon = BmlCardParser.cardNetworkIcon(acc)
if (networkIcon != null) {
b.ivDropdownCardLogo.setImageResource(networkIcon)
b.ivDropdownCardLogo.visibility = View.VISIBLE
} else {
b.ivDropdownCardLogo.visibility = View.GONE
when {
networkIcon != null -> {
b.ivDropdownCardLogo.setImageResource(networkIcon)
b.ivDropdownCardLogo.visibility = View.VISIBLE
}
acc.bank == "BML" -> {
b.ivDropdownCardLogo.setImageResource(R.drawable.bml_logo_vector)
b.ivDropdownCardLogo.visibility = View.VISIBLE
}
acc.bank == "FAHIPAY" -> {
b.ivDropdownCardLogo.setImageResource(R.drawable.fahipay_logo)
b.ivDropdownCardLogo.visibility = View.VISIBLE
}
acc.bank == "MIB" -> {
val hash = acc.profileImageHash
val cached = hash?.let { dropdownProfileImageCache[it] }
if (cached != null) {
b.ivDropdownCardLogo.setImageBitmap(cached)
} else {
b.ivDropdownCardLogo.setImageResource(R.drawable.mib_logo)
if (hash != null) {
val app = requireActivity().application as BasedBankApp
viewLifecycleOwner.lifecycleScope.launch {
val bitmap = withContext(Dispatchers.IO) {
try {
val sess = app.anyMibSession() ?: return@withContext null
val b64 = app.anyMibFlow()?.fetchProfileImage(sess, hash) ?: return@withContext null
val bytes = android.util.Base64.decode(b64, android.util.Base64.DEFAULT)
android.graphics.BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
} catch (_: Exception) { null }
}
if (bitmap != null) {
dropdownProfileImageCache[hash] = bitmap
accountDropdownAdapter?.notifyDataSetChanged()
}
}
}
}
b.ivDropdownCardLogo.visibility = View.VISIBLE
}
else -> b.ivDropdownCardLogo.visibility = View.GONE
}
b.root
}
+10
View File
@@ -14,6 +14,16 @@
android:gravity="center_vertical"
android:foreground="?attr/selectableItemBackground">
<!-- Bank / profile logo -->
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/ivBankLogo"
android:layout_width="40dp"
android:layout_height="40dp"
android:scaleType="fitCenter"
android:layout_marginEnd="12dp"
app:shapeAppearanceOverlay="@style/ShapeAppearance.Circle"
xmlns:app="http://schemas.android.com/apk/res-auto" />
<!-- Left: name / number -->
<LinearLayout
android:layout_width="0dp"
@@ -8,13 +8,15 @@
android:paddingHorizontal="16dp"
android:paddingVertical="12dp">
<ImageView
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/ivDropdownCardLogo"
android:layout_width="36dp"
android:layout_height="24dp"
android:layout_height="36dp"
android:scaleType="fitCenter"
android:layout_marginEnd="10dp"
android:visibility="gone" />
android:visibility="gone"
app:shapeAppearanceOverlay="@style/ShapeAppearance.Circle"
xmlns:app="http://schemas.android.com/apk/res-auto" />
<LinearLayout
android:layout_width="match_parent"