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.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.graphics.Bitmap
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import sh.sar.basedbank.R
import sh.sar.basedbank.api.models.BankAccount import sh.sar.basedbank.api.models.BankAccount
import sh.sar.basedbank.databinding.ItemAccountBinding import sh.sar.basedbank.databinding.ItemAccountBinding
import sh.sar.basedbank.databinding.ItemCardBinding import sh.sar.basedbank.databinding.ItemCardBinding
@@ -17,7 +19,9 @@ import sh.sar.basedbank.util.AccountListParser
class AccountsAdapter( class AccountsAdapter(
accounts: List<BankAccount>, 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>() { ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
var onTransferClick: ((BankAccount) -> Unit)? = null var onTransferClick: ((BankAccount) -> Unit)? = null
@@ -112,6 +116,8 @@ class AccountsAdapter(
private inner class AccountViewHolder(private val binding: ItemAccountBinding) : private inner class AccountViewHolder(private val binding: ItemAccountBinding) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
private var boundHash: String? = null
fun bind(account: BankAccount, display: AccountListDisplay) { fun bind(account: BankAccount, display: AccountListDisplay) {
binding.tvAccountName.text = display.name binding.tvAccountName.text = display.name
binding.tvAccountNumber.text = display.number binding.tvAccountNumber.text = display.number
@@ -123,6 +129,23 @@ class AccountsAdapter(
copyToClipboard(it.context, display.number) copyToClipboard(it.context, display.number)
true 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 package sh.sar.basedbank.ui.home
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Bundle import android.os.Bundle
import android.util.Base64
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@@ -8,7 +11,12 @@ import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager 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.R
import sh.sar.basedbank.databinding.FragmentAccountsBinding import sh.sar.basedbank.databinding.FragmentAccountsBinding
@@ -18,6 +26,7 @@ class AccountsFragment : Fragment() {
private val binding get() = _binding!! private val binding get() = _binding!!
private val viewModel: HomeViewModel by activityViewModels() private val viewModel: HomeViewModel by activityViewModels()
private lateinit var adapter: AccountsAdapter private lateinit var adapter: AccountsAdapter
private val profileImageCache = mutableMapOf<String, Bitmap>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentAccountsBinding.inflate(inflater, container, false) _binding = FragmentAccountsBinding.inflate(inflater, container, false)
@@ -25,9 +34,30 @@ class AccountsFragment : Fragment() {
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
adapter = AccountsAdapter(emptyList()) { account -> val app = requireActivity().application as BasedBankApp
(activity as? HomeActivity)?.showWithBackStack(AccountHistoryFragment.newInstance(account)) 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 -> adapter.onTransferClick = { account ->
(activity as? HomeActivity)?.navigateTo(R.id.nav_transfer, TransferFragment.newInstanceFrom(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) // BML QR merchant payment mode (set when navigated from a card QR scan)
private var bmlQrInfo: BmlQrPayInfo? = null private var bmlQrInfo: BmlQrPayInfo? = null
private var bmlGatewayQr = false // true for pay.bml.com.mv QRs (requires pre-initiate step) 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 // BML business profile OTP flow state
private enum class BmlOtpState { NONE, SELECTING_CHANNEL, AWAITING_OTP } 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.tvDropdownBalance.text = if (hide && balance.isNotBlank()) maskAmount(balance) else balance
b.root.alpha = if (inactive) 0.4f else 1f b.root.alpha = if (inactive) 0.4f else 1f
val networkIcon = BmlCardParser.cardNetworkIcon(acc) val networkIcon = BmlCardParser.cardNetworkIcon(acc)
if (networkIcon != null) { when {
b.ivDropdownCardLogo.setImageResource(networkIcon) networkIcon != null -> {
b.ivDropdownCardLogo.visibility = View.VISIBLE b.ivDropdownCardLogo.setImageResource(networkIcon)
} else { b.ivDropdownCardLogo.visibility = View.VISIBLE
b.ivDropdownCardLogo.visibility = View.GONE }
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 b.root
} }
+10
View File
@@ -14,6 +14,16 @@
android:gravity="center_vertical" android:gravity="center_vertical"
android:foreground="?attr/selectableItemBackground"> 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 --> <!-- Left: name / number -->
<LinearLayout <LinearLayout
android:layout_width="0dp" android:layout_width="0dp"
@@ -8,13 +8,15 @@
android:paddingHorizontal="16dp" android:paddingHorizontal="16dp"
android:paddingVertical="12dp"> android:paddingVertical="12dp">
<ImageView <com.google.android.material.imageview.ShapeableImageView
android:id="@+id/ivDropdownCardLogo" android:id="@+id/ivDropdownCardLogo"
android:layout_width="36dp" android:layout_width="36dp"
android:layout_height="24dp" android:layout_height="36dp"
android:scaleType="fitCenter" android:scaleType="fitCenter"
android:layout_marginEnd="10dp" 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 <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"