forked from shihaam/thijooree
added support for custom per-profile image for BML and Fahipay, MIB works pending
This commit is contained in:
@@ -39,7 +39,7 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
|
|||||||
.connectTimeout(30, TimeUnit.SECONDS)
|
.connectTimeout(30, TimeUnit.SECONDS)
|
||||||
.readTimeout(30, TimeUnit.SECONDS)
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
.writeTimeout(30, TimeUnit.SECONDS)
|
.writeTimeout(30, TimeUnit.SECONDS)
|
||||||
.addInterceptor { chain ->
|
.addNetworkInterceptor { chain ->
|
||||||
val req = chain.request().newBuilder()
|
val req = chain.request().newBuilder()
|
||||||
.header("User-Agent", "android/1.0")
|
.header("User-Agent", "android/1.0")
|
||||||
.header("Accept", "application/json")
|
.header("Accept", "application/json")
|
||||||
@@ -366,6 +366,34 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
|
|||||||
return resp.optString("profileImage").takeIf { it.isNotBlank() }
|
return resp.optString("profileImage").takeIf { it.isNotBlank() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads a profile image via P40.
|
||||||
|
* [imageBase64] is a base64-encoded JPEG string.
|
||||||
|
* Returns the new imageHash on success, or null on failure.
|
||||||
|
*/
|
||||||
|
fun uploadProfileImage(session: MibSession, profile: MibProfile, imageBase64: String): String? {
|
||||||
|
val payload = baseData(session, "P40").apply {
|
||||||
|
put("profileId", profile.profileId)
|
||||||
|
put("profileImage", imageBase64)
|
||||||
|
}
|
||||||
|
val resp = doRequest(session, payload, "n")
|
||||||
|
if (!resp.optBoolean("success", false)) return null
|
||||||
|
return resp.optString("imageHash").takeIf { it.isNotBlank() }
|
||||||
|
?: resp.optString("customerImage").takeIf { it.isNotBlank() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the profile image via P42.
|
||||||
|
* Returns true on success.
|
||||||
|
*/
|
||||||
|
fun deleteProfileImage(session: MibSession, profile: MibProfile): Boolean {
|
||||||
|
val payload = baseData(session, "P42").apply {
|
||||||
|
put("profileId", profile.profileId)
|
||||||
|
}
|
||||||
|
val resp = doRequest(session, payload, "n")
|
||||||
|
return resp.optBoolean("success", false)
|
||||||
|
}
|
||||||
|
|
||||||
private fun post(body: FormBody): String {
|
private fun post(body: FormBody): String {
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url(BASE_URL)
|
.url(BASE_URL)
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ 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) */
|
/** Optional loader for MIB per-profile images: (hash, onLoaded) */
|
||||||
private val profileImageLoader: ((String, (Bitmap) -> Unit) -> Unit)? = null
|
private val profileImageLoader: ((String, (Bitmap) -> Unit) -> Unit)? = null,
|
||||||
|
/** Optional loader for local (BML/Fahipay) profile images: (loginTag, profileId, onLoaded) */
|
||||||
|
private val localProfileImageLoader: ((String, String, (Bitmap) -> Unit) -> Unit)? = null
|
||||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
|
|
||||||
var onTransferClick: ((BankAccount) -> Unit)? = null
|
var onTransferClick: ((BankAccount) -> Unit)? = null
|
||||||
@@ -141,9 +143,16 @@ class AccountsAdapter(
|
|||||||
|
|
||||||
val hash = account.profileImageHash
|
val hash = account.profileImageHash
|
||||||
boundHash = hash
|
boundHash = hash
|
||||||
if (account.bank == "MIB" && hash != null && profileImageLoader != null) {
|
when {
|
||||||
profileImageLoader.invoke(hash) { bitmap ->
|
account.bank == "MIB" && hash != null && profileImageLoader != null -> {
|
||||||
if (boundHash == hash) binding.ivBankLogo.setImageBitmap(bitmap)
|
profileImageLoader.invoke(hash) { bitmap ->
|
||||||
|
if (boundHash == hash) binding.ivBankLogo.setImageBitmap(bitmap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(account.bank == "BML" || account.bank == "FAHIPAY") && localProfileImageLoader != null -> {
|
||||||
|
localProfileImageLoader.invoke(account.loginTag, account.profileId) { bitmap ->
|
||||||
|
if (boundHash == hash) binding.ivBankLogo.setImageBitmap(bitmap)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import kotlinx.coroutines.withContext
|
|||||||
import sh.sar.basedbank.BasedBankApp
|
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
|
||||||
|
import sh.sar.basedbank.util.ProfileImageStore
|
||||||
|
|
||||||
class AccountsFragment : Fragment() {
|
class AccountsFragment : Fragment() {
|
||||||
|
|
||||||
@@ -56,6 +57,25 @@ class AccountsFragment : Fragment() {
|
|||||||
onLoaded(bitmap)
|
onLoaded(bitmap)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
localProfileImageLoader = { loginTag, profileId, onLoaded ->
|
||||||
|
val cacheKey = "$loginTag|$profileId"
|
||||||
|
profileImageCache[cacheKey]?.let { onLoaded(it); return@AccountsAdapter }
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
val bitmap = withContext(Dispatchers.IO) {
|
||||||
|
val ctx = requireContext()
|
||||||
|
if (loginTag.startsWith("bml_") && profileId.isNotBlank()) {
|
||||||
|
ProfileImageStore.load(ctx, ProfileImageStore.bmlKey(profileId))
|
||||||
|
} else if (loginTag.startsWith("fahipay_")) {
|
||||||
|
val loginId = ProfileImageStore.loginIdFromTag(loginTag)
|
||||||
|
ProfileImageStore.load(ctx, ProfileImageStore.fahipayKey(loginId))
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
if (bitmap != null) {
|
||||||
|
profileImageCache[cacheKey] = bitmap
|
||||||
|
onLoaded(bitmap)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
adapter.onTransferClick = { account ->
|
adapter.onTransferClick = { account ->
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import sh.sar.basedbank.R
|
|||||||
import sh.sar.basedbank.api.mib.MibContactsClient
|
import sh.sar.basedbank.api.mib.MibContactsClient
|
||||||
import sh.sar.basedbank.databinding.SheetContactPickerBinding
|
import sh.sar.basedbank.databinding.SheetContactPickerBinding
|
||||||
import sh.sar.basedbank.util.AccountListParser
|
import sh.sar.basedbank.util.AccountListParser
|
||||||
|
import sh.sar.basedbank.util.ProfileImageStore
|
||||||
import sh.sar.basedbank.util.RecentsCache
|
import sh.sar.basedbank.util.RecentsCache
|
||||||
import sh.sar.basedbank.util.bmlapi.BmlCardParser
|
import sh.sar.basedbank.util.bmlapi.BmlCardParser
|
||||||
import sh.sar.basedbank.util.bmlapi.BmlDashboardParser
|
import sh.sar.basedbank.util.bmlapi.BmlDashboardParser
|
||||||
@@ -237,13 +238,15 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
|||||||
"MIB" -> R.drawable.mib_logo
|
"MIB" -> R.drawable.mib_logo
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
val localKey = localImageKeyFor(acc)
|
||||||
|
if (localKey != null) profileImageHashes.add("local:$localKey")
|
||||||
items.add(ContactPickerAdapter.PickerItem.Row(
|
items.add(ContactPickerAdapter.PickerItem.Row(
|
||||||
accountNumber = acc.accountNumber,
|
accountNumber = acc.accountNumber,
|
||||||
displayName = acc.accountBriefName,
|
displayName = acc.accountBriefName,
|
||||||
subtitle = acc.accountNumber,
|
subtitle = acc.accountNumber,
|
||||||
colorHex = "#FE860E",
|
colorHex = "#FE860E",
|
||||||
isSameAsFrom = isSame,
|
isSameAsFrom = isSame,
|
||||||
imageHash = acc.profileImageHash,
|
imageHash = acc.profileImageHash ?: localKey?.let { "local:$it" },
|
||||||
inactiveReason = if (isSame) null
|
inactiveReason = if (isSame) null
|
||||||
else if (fromIsCard && acc.loginTag != fromLoginTag) "Cards can only be used within the same BML account"
|
else if (fromIsCard && acc.loginTag != fromLoginTag) "Cards can only be used within the same BML account"
|
||||||
else currencyMismatchReason(fromCurrency, acc.currencyName),
|
else currencyMismatchReason(fromCurrency, acc.currencyName),
|
||||||
@@ -267,13 +270,15 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
|||||||
else AccountListParser.from(acc)?.balance ?: "${acc.currencyName} ${acc.availableBalance}"
|
else AccountListParser.from(acc)?.balance ?: "${acc.currencyName} ${acc.availableBalance}"
|
||||||
val balance = parsedBalance?.let { if (hide) maskAmount(it) else it }
|
val balance = parsedBalance?.let { if (hide) maskAmount(it) else it }
|
||||||
val logoRes = BmlCardParser.cardNetworkIcon(acc) ?: R.drawable.bml_logo_vector
|
val logoRes = BmlCardParser.cardNetworkIcon(acc) ?: R.drawable.bml_logo_vector
|
||||||
|
val localKey = localImageKeyFor(acc)
|
||||||
|
if (localKey != null) profileImageHashes.add("local:$localKey")
|
||||||
items.add(ContactPickerAdapter.PickerItem.Row(
|
items.add(ContactPickerAdapter.PickerItem.Row(
|
||||||
accountNumber = acc.accountNumber,
|
accountNumber = acc.accountNumber,
|
||||||
displayName = acc.accountBriefName,
|
displayName = acc.accountBriefName,
|
||||||
subtitle = acc.accountNumber,
|
subtitle = acc.accountNumber,
|
||||||
colorHex = "#FE860E",
|
colorHex = "#FE860E",
|
||||||
isSameAsFrom = isSame,
|
isSameAsFrom = isSame,
|
||||||
imageHash = acc.profileImageHash,
|
imageHash = acc.profileImageHash ?: localKey?.let { "local:$it" },
|
||||||
inactiveReason = if (isSame) null
|
inactiveReason = if (isSame) null
|
||||||
else if (!isActive) acc.statusDesc
|
else if (!isActive) acc.statusDesc
|
||||||
else if (acc.loginTag != fromLoginTag) "Cards can only be used within the same BML account"
|
else if (acc.loginTag != fromLoginTag) "Cards can only be used within the same BML account"
|
||||||
@@ -310,6 +315,17 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
|||||||
|
|
||||||
private fun fetchImage(hash: String) {
|
private fun fetchImage(hash: String) {
|
||||||
if (!pendingHashes.add(hash)) return
|
if (!pendingHashes.add(hash)) return
|
||||||
|
// Local image keys for BML/Fahipay (prefixed with "local:")
|
||||||
|
if (hash.startsWith("local:")) {
|
||||||
|
val key = hash.removePrefix("local:")
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
val bitmap = ProfileImageStore.load(requireContext(), key) ?: run {
|
||||||
|
pendingHashes.remove(hash); return@launch
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) { pagerAdapter.updateImage(hash, bitmap) }
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
val sess = session ?: return
|
val sess = session ?: return
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
@@ -337,6 +353,16 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
|||||||
private fun currencyMismatchReason(fromCurrency: String, toCurrency: String): String? =
|
private fun currencyMismatchReason(fromCurrency: String, toCurrency: String): String? =
|
||||||
if (fromCurrency == "MVR" && toCurrency == "USD") "Cannot transfer from MVR to USD account" else null
|
if (fromCurrency == "MVR" && toCurrency == "USD") "Cannot transfer from MVR to USD account" else null
|
||||||
|
|
||||||
|
/** Returns the ProfileImageStore key for BML/Fahipay accounts, or null for MIB/others. */
|
||||||
|
private fun localImageKeyFor(acc: sh.sar.basedbank.api.models.BankAccount): String? = when (acc.bank) {
|
||||||
|
"BML" -> if (acc.profileId.isNotBlank()) sh.sar.basedbank.util.ProfileImageStore.bmlKey(acc.profileId) else null
|
||||||
|
"FAHIPAY" -> {
|
||||||
|
val loginId = sh.sar.basedbank.util.ProfileImageStore.loginIdFromTag(acc.loginTag)
|
||||||
|
if (loginId.isNotBlank()) sh.sar.basedbank.util.ProfileImageStore.fahipayKey(loginId) else null
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
_binding = null
|
_binding = null
|
||||||
|
|||||||
@@ -437,12 +437,51 @@ class PayMvQrFragment : Fragment() {
|
|||||||
b.ivDropdownCardLogo.visibility = View.VISIBLE
|
b.ivDropdownCardLogo.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
acc.bank == "BML" -> {
|
acc.bank == "BML" -> {
|
||||||
b.ivDropdownCardLogo.setImageResource(R.drawable.bml_logo_vector)
|
val localKey = sh.sar.basedbank.util.ProfileImageStore.bmlKey(acc.profileId)
|
||||||
b.ivDropdownCardLogo.visibility = View.VISIBLE
|
val cachedLocal = dropdownProfileImageCache[localKey]
|
||||||
|
val imageView = b.ivDropdownCardLogo
|
||||||
|
imageView.tag = localKey
|
||||||
|
if (cachedLocal != null) {
|
||||||
|
imageView.setImageBitmap(cachedLocal)
|
||||||
|
} else {
|
||||||
|
imageView.setImageResource(R.drawable.bml_logo_vector)
|
||||||
|
if (acc.profileId.isNotBlank()) {
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
val bitmap = withContext(Dispatchers.IO) {
|
||||||
|
sh.sar.basedbank.util.ProfileImageStore.load(requireContext(), localKey)
|
||||||
|
}
|
||||||
|
if (bitmap != null) {
|
||||||
|
dropdownProfileImageCache[localKey] = bitmap
|
||||||
|
if (imageView.tag == localKey) imageView.setImageBitmap(bitmap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imageView.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
acc.bank == "FAHIPAY" -> {
|
acc.bank == "FAHIPAY" -> {
|
||||||
b.ivDropdownCardLogo.setImageResource(R.drawable.fahipay_logo)
|
val loginId = sh.sar.basedbank.util.ProfileImageStore.loginIdFromTag(acc.loginTag)
|
||||||
b.ivDropdownCardLogo.visibility = View.VISIBLE
|
val localKey = sh.sar.basedbank.util.ProfileImageStore.fahipayKey(loginId)
|
||||||
|
val cachedLocal = dropdownProfileImageCache[localKey]
|
||||||
|
val imageView = b.ivDropdownCardLogo
|
||||||
|
imageView.tag = localKey
|
||||||
|
if (cachedLocal != null) {
|
||||||
|
imageView.setImageBitmap(cachedLocal)
|
||||||
|
} else {
|
||||||
|
imageView.setImageResource(R.drawable.fahipay_logo)
|
||||||
|
if (loginId.isNotBlank()) {
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
val bitmap = withContext(Dispatchers.IO) {
|
||||||
|
sh.sar.basedbank.util.ProfileImageStore.load(requireContext(), localKey)
|
||||||
|
}
|
||||||
|
if (bitmap != null) {
|
||||||
|
dropdownProfileImageCache[localKey] = bitmap
|
||||||
|
if (imageView.tag == localKey) imageView.setImageBitmap(bitmap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imageView.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
acc.bank == "MIB" -> {
|
acc.bank == "MIB" -> {
|
||||||
val hash = acc.profileImageHash
|
val hash = acc.profileImageHash
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ package sh.sar.basedbank.ui.home
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Base64
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -10,6 +14,8 @@ import android.view.ViewGroup
|
|||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
@@ -28,6 +34,7 @@ import sh.sar.basedbank.util.ContactsCache
|
|||||||
import sh.sar.basedbank.util.CredentialStore
|
import sh.sar.basedbank.util.CredentialStore
|
||||||
import sh.sar.basedbank.util.FinancingCache
|
import sh.sar.basedbank.util.FinancingCache
|
||||||
import sh.sar.basedbank.util.ForeignLimitsCache
|
import sh.sar.basedbank.util.ForeignLimitsCache
|
||||||
|
import sh.sar.basedbank.util.ProfileImageStore
|
||||||
import sh.sar.basedbank.util.RecentsCache
|
import sh.sar.basedbank.util.RecentsCache
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
@@ -38,6 +45,8 @@ import kotlinx.coroutines.withContext
|
|||||||
import sh.sar.basedbank.api.bml.BmlActivationResult
|
import sh.sar.basedbank.api.bml.BmlActivationResult
|
||||||
import sh.sar.basedbank.api.bml.BmlLoginFlow
|
import sh.sar.basedbank.api.bml.BmlLoginFlow
|
||||||
import sh.sar.basedbank.api.bml.BmlOtpChannel
|
import sh.sar.basedbank.api.bml.BmlOtpChannel
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
class SettingsLoginsFragment : Fragment() {
|
class SettingsLoginsFragment : Fragment() {
|
||||||
|
|
||||||
@@ -45,6 +54,248 @@ class SettingsLoginsFragment : Fragment() {
|
|||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
private val viewModel: HomeViewModel by activityViewModels()
|
private val viewModel: HomeViewModel by activityViewModels()
|
||||||
|
|
||||||
|
// ── Profile image picker state ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
private sealed class PendingImageTarget {
|
||||||
|
data class Mib(val loginId: String, val profile: MibProfile) : PendingImageTarget()
|
||||||
|
data class Bml(val profileId: String, val refreshImage: (Bitmap?) -> Unit) : PendingImageTarget()
|
||||||
|
data class Fahipay(val loginId: String, val refreshImage: (Bitmap?) -> Unit) : PendingImageTarget()
|
||||||
|
}
|
||||||
|
private var pendingImageTarget: PendingImageTarget? = null
|
||||||
|
private var cameraPhotoUri: Uri? = null
|
||||||
|
|
||||||
|
private val galleryLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
|
||||||
|
if (uri == null) return@registerForActivityResult
|
||||||
|
val bitmap = loadAndScaleBitmap(uri) ?: return@registerForActivityResult
|
||||||
|
handlePickedImage(bitmap)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val cameraLauncher = registerForActivityResult(ActivityResultContracts.TakePicture()) { success ->
|
||||||
|
if (!success) return@registerForActivityResult
|
||||||
|
val uri = cameraPhotoUri ?: return@registerForActivityResult
|
||||||
|
val bitmap = loadAndScaleBitmap(uri) ?: return@registerForActivityResult
|
||||||
|
handlePickedImage(bitmap)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadAndScaleBitmap(uri: Uri): Bitmap? {
|
||||||
|
return try {
|
||||||
|
val ctx = requireContext()
|
||||||
|
val inputStream = ctx.contentResolver.openInputStream(uri) ?: return null
|
||||||
|
val original = BitmapFactory.decodeStream(inputStream)
|
||||||
|
inputStream.close()
|
||||||
|
if (original == null) return null
|
||||||
|
val maxDim = 512
|
||||||
|
val scale = minOf(maxDim.toFloat() / original.width, maxDim.toFloat() / original.height, 1f)
|
||||||
|
if (scale < 1f) {
|
||||||
|
Bitmap.createScaledBitmap(original, (original.width * scale).toInt(), (original.height * scale).toInt(), true)
|
||||||
|
} else original
|
||||||
|
} catch (_: Exception) { null }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handlePickedImage(bitmap: Bitmap) {
|
||||||
|
val target = pendingImageTarget ?: return
|
||||||
|
when (target) {
|
||||||
|
is PendingImageTarget.Mib -> uploadMibProfileImage(target.loginId, target.profile, bitmap)
|
||||||
|
is PendingImageTarget.Bml -> {
|
||||||
|
ProfileImageStore.save(requireContext(), ProfileImageStore.bmlKey(target.profileId), bitmap)
|
||||||
|
target.refreshImage(bitmap)
|
||||||
|
}
|
||||||
|
is PendingImageTarget.Fahipay -> {
|
||||||
|
ProfileImageStore.save(requireContext(), ProfileImageStore.fahipayKey(target.loginId), bitmap)
|
||||||
|
target.refreshImage(bitmap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun uploadMibProfileImage(loginId: String, profile: MibProfile, bitmap: Bitmap) {
|
||||||
|
val ctx = requireContext()
|
||||||
|
val app = requireActivity().application as BasedBankApp
|
||||||
|
val progress = MaterialAlertDialogBuilder(ctx)
|
||||||
|
.setMessage(getString(R.string.profile_image_uploading))
|
||||||
|
.setCancelable(false)
|
||||||
|
.show()
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
val result = withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val session = app.mibSessions[loginId] ?: app.anyMibSession() ?: return@withContext null
|
||||||
|
val flow = app.mibLoginFlows[loginId] ?: return@withContext null
|
||||||
|
flow.switchProfile(session, profile)
|
||||||
|
// MIB server enforces a small payload limit (~4KB); scale to 100px max
|
||||||
|
val mibMax = 100
|
||||||
|
val mibScale = minOf(mibMax.toFloat() / bitmap.width, mibMax.toFloat() / bitmap.height, 1f)
|
||||||
|
val mibBitmap = if (mibScale < 1f)
|
||||||
|
Bitmap.createScaledBitmap(bitmap, (bitmap.width * mibScale).toInt(), (bitmap.height * mibScale).toInt(), true)
|
||||||
|
else bitmap
|
||||||
|
val baos = ByteArrayOutputStream()
|
||||||
|
mibBitmap.compress(Bitmap.CompressFormat.JPEG, 50, baos)
|
||||||
|
val base64 = Base64.encodeToString(baos.toByteArray(), Base64.NO_WRAP)
|
||||||
|
flow.uploadProfileImage(session, profile, base64)
|
||||||
|
} catch (_: Exception) { null }
|
||||||
|
}
|
||||||
|
progress.dismiss()
|
||||||
|
if (result == null) {
|
||||||
|
MaterialAlertDialogBuilder(ctx)
|
||||||
|
.setMessage(getString(R.string.profile_image_upload_failed))
|
||||||
|
.setPositiveButton(R.string.close, null)
|
||||||
|
.show()
|
||||||
|
} else {
|
||||||
|
clearAllCaches(ctx)
|
||||||
|
(activity as? HomeActivity)?.relogin()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showImagePickerMenu(anchor: View, target: PendingImageTarget, currentBitmap: Bitmap?) {
|
||||||
|
val ctx = requireContext()
|
||||||
|
val dp = ctx.resources.displayMetrics.density
|
||||||
|
|
||||||
|
val items = mutableListOf<Triple<Int, String, () -> Unit>>()
|
||||||
|
items += Triple(R.drawable.ic_image, getString(R.string.profile_image_select)) {
|
||||||
|
pendingImageTarget = target
|
||||||
|
galleryLauncher.launch("image/*")
|
||||||
|
}
|
||||||
|
items += Triple(R.drawable.ic_camera, getString(R.string.profile_image_camera)) {
|
||||||
|
pendingImageTarget = target
|
||||||
|
val photoFile = File(ctx.cacheDir, "profile_photo_tmp.jpg")
|
||||||
|
val uri = FileProvider.getUriForFile(ctx, "${ctx.packageName}.fileprovider", photoFile)
|
||||||
|
cameraPhotoUri = uri
|
||||||
|
cameraLauncher.launch(uri)
|
||||||
|
}
|
||||||
|
if (target is PendingImageTarget.Mib || currentBitmap != null || hasSavedImage(ctx, target)) {
|
||||||
|
items += Triple(R.drawable.ic_delete, getString(R.string.profile_image_remove)) {
|
||||||
|
removeProfileImage(ctx, target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val list = LinearLayout(ctx).apply {
|
||||||
|
orientation = LinearLayout.VERTICAL
|
||||||
|
val vp = (8 * dp).toInt()
|
||||||
|
setPadding(0, vp, 0, vp)
|
||||||
|
}
|
||||||
|
for ((iconRes, label, action) in items) {
|
||||||
|
val iconSize = (24 * dp).toInt()
|
||||||
|
val row = LinearLayout(ctx).apply {
|
||||||
|
orientation = LinearLayout.HORIZONTAL
|
||||||
|
gravity = Gravity.CENTER_VERTICAL
|
||||||
|
val ta = ctx.obtainStyledAttributes(intArrayOf(android.R.attr.selectableItemBackground))
|
||||||
|
background = ta.getDrawable(0); ta.recycle()
|
||||||
|
isClickable = true; isFocusable = true
|
||||||
|
val hp = (24 * dp).toInt(); val vp2 = (12 * dp).toInt()
|
||||||
|
setPadding(hp, vp2, hp, vp2)
|
||||||
|
}
|
||||||
|
val iconColor = com.google.android.material.color.MaterialColors.getColor(
|
||||||
|
requireView(), com.google.android.material.R.attr.colorOnSurface, android.graphics.Color.BLACK)
|
||||||
|
val icon = ImageView(ctx).apply {
|
||||||
|
setImageResource(iconRes)
|
||||||
|
imageTintList = android.content.res.ColorStateList.valueOf(iconColor)
|
||||||
|
}
|
||||||
|
val tv = TextView(ctx).apply {
|
||||||
|
text = label
|
||||||
|
setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodyLarge)
|
||||||
|
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f).apply {
|
||||||
|
marginStart = (12 * dp).toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
row.addView(icon, LinearLayout.LayoutParams(iconSize, iconSize))
|
||||||
|
row.addView(tv)
|
||||||
|
list.addView(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
val dialog = MaterialAlertDialogBuilder(ctx)
|
||||||
|
.setTitle(R.string.profile_image_title)
|
||||||
|
.setView(list)
|
||||||
|
.setNegativeButton(R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
|
||||||
|
val rows = (0 until list.childCount).map { list.getChildAt(it) }
|
||||||
|
rows.forEachIndexed { i, row ->
|
||||||
|
row.setOnClickListener {
|
||||||
|
dialog.dismiss()
|
||||||
|
items[i].third()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasSavedImage(ctx: Context, target: PendingImageTarget): Boolean = when (target) {
|
||||||
|
is PendingImageTarget.Mib -> target.profile.customerImage != null
|
||||||
|
is PendingImageTarget.Bml -> ProfileImageStore.exists(ctx, ProfileImageStore.bmlKey(target.profileId))
|
||||||
|
is PendingImageTarget.Fahipay -> ProfileImageStore.exists(ctx, ProfileImageStore.fahipayKey(target.loginId))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeProfileImage(ctx: Context, target: PendingImageTarget) {
|
||||||
|
when (target) {
|
||||||
|
is PendingImageTarget.Mib -> {
|
||||||
|
val app = requireActivity().application as BasedBankApp
|
||||||
|
val progress = MaterialAlertDialogBuilder(ctx)
|
||||||
|
.setMessage(getString(R.string.profile_image_deleting))
|
||||||
|
.setCancelable(false)
|
||||||
|
.show()
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val session = app.mibSessions[target.loginId] ?: app.anyMibSession() ?: return@withContext
|
||||||
|
val flow = app.mibLoginFlows[target.loginId] ?: return@withContext
|
||||||
|
flow.switchProfile(session, target.profile)
|
||||||
|
flow.deleteProfileImage(session, target.profile)
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
}
|
||||||
|
progress.dismiss()
|
||||||
|
clearAllCaches(ctx)
|
||||||
|
(activity as? HomeActivity)?.relogin()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is PendingImageTarget.Bml -> {
|
||||||
|
ProfileImageStore.delete(ctx, ProfileImageStore.bmlKey(target.profileId))
|
||||||
|
target.refreshImage(null)
|
||||||
|
}
|
||||||
|
is PendingImageTarget.Fahipay -> {
|
||||||
|
ProfileImageStore.delete(ctx, ProfileImageStore.fahipayKey(target.loginId))
|
||||||
|
target.refreshImage(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun makePencilButton(ctx: Context): ImageView {
|
||||||
|
val dp = ctx.resources.displayMetrics.density
|
||||||
|
val size = (32 * dp).toInt()
|
||||||
|
return ImageView(ctx).apply {
|
||||||
|
setImageResource(R.drawable.ic_edit)
|
||||||
|
val ta = ctx.obtainStyledAttributes(intArrayOf(android.R.attr.selectableItemBackgroundBorderless))
|
||||||
|
background = ta.getDrawable(0); ta.recycle()
|
||||||
|
isClickable = true; isFocusable = true
|
||||||
|
layoutParams = LinearLayout.LayoutParams(size, size).apply {
|
||||||
|
marginStart = (4 * dp).toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun makeCircleAvatarView(ctx: Context, sizeDp: Int): ImageView {
|
||||||
|
val dp = ctx.resources.displayMetrics.density
|
||||||
|
val size = (sizeDp * dp).toInt()
|
||||||
|
return ImageView(ctx).apply {
|
||||||
|
layoutParams = LinearLayout.LayoutParams(size, size)
|
||||||
|
scaleType = ImageView.ScaleType.CENTER_CROP
|
||||||
|
clipToOutline = true
|
||||||
|
outlineProvider = object : android.view.ViewOutlineProvider() {
|
||||||
|
override fun getOutline(view: View, outline: android.graphics.Outline) {
|
||||||
|
outline.setOval(0, 0, view.width, view.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setAvatarBitmap(iv: ImageView, bitmap: Bitmap?) {
|
||||||
|
if (bitmap != null) {
|
||||||
|
iv.setImageBitmap(bitmap)
|
||||||
|
iv.imageTintList = null
|
||||||
|
} else {
|
||||||
|
iv.setImageResource(R.drawable.ic_image)
|
||||||
|
val color = com.google.android.material.color.MaterialColors.getColor(
|
||||||
|
iv, com.google.android.material.R.attr.colorOnSurface, android.graphics.Color.BLACK)
|
||||||
|
iv.imageTintList = android.content.res.ColorStateList.valueOf(color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
_binding = FragmentSettingsLoginsBinding.inflate(inflater, container, false)
|
_binding = FragmentSettingsLoginsBinding.inflate(inflater, container, false)
|
||||||
return binding.root
|
return binding.root
|
||||||
@@ -101,18 +352,7 @@ class SettingsLoginsFragment : Fragment() {
|
|||||||
val profile = store.loadFahipayUserProfile(loginId)
|
val profile = store.loadFahipayUserProfile(loginId)
|
||||||
val displayName = profile?.fullName?.takeIf { it.isNotBlank() } ?: getString(R.string.fahipay_name)
|
val displayName = profile?.fullName?.takeIf { it.isNotBlank() } ?: getString(R.string.fahipay_name)
|
||||||
addLoginRow(container, R.drawable.fahipay_logo, displayName) {
|
addLoginRow(container, R.drawable.fahipay_logo, displayName) {
|
||||||
val hide = viewModel.hideAmounts.value ?: false
|
showFahipayLoginDetails(store, loginId, profile)
|
||||||
val masked = "••••••"
|
|
||||||
showLoginDetails(
|
|
||||||
title = getString(R.string.fahipay_name),
|
|
||||||
details = buildString {
|
|
||||||
if (!profile?.fullName.isNullOrBlank()) appendLine("${getString(R.string.login_detail_name)}: ${profile!!.fullName}")
|
|
||||||
if (!profile?.email.isNullOrBlank()) appendLine("${getString(R.string.login_detail_email)}: ${if (hide) masked else profile!!.email}")
|
|
||||||
if (!profile?.mobile.isNullOrBlank()) appendLine("${getString(R.string.login_detail_mobile)}: ${if (hide) masked else profile!!.mobile}")
|
|
||||||
if (!profile?.nid.isNullOrBlank()) appendLine("${getString(R.string.login_detail_id_card)}: ${if (hide) masked else profile!!.nid}")
|
|
||||||
}.trim(),
|
|
||||||
onLogout = { confirmLogout(getString(R.string.fahipay_name)) { logoutFahipay(store, loginId) } }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -219,8 +459,21 @@ class SettingsLoginsFragment : Fragment() {
|
|||||||
alpha = 0.6f
|
alpha = 0.6f
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
val toggle = MaterialSwitch(ctx).apply { isChecked = p.profileId !in hidden }
|
val pencil = makePencilButton(ctx).apply {
|
||||||
|
alpha = 0.38f
|
||||||
|
isEnabled = false
|
||||||
|
}
|
||||||
|
val toggle = MaterialSwitch(ctx).apply {
|
||||||
|
isChecked = p.profileId !in hidden
|
||||||
|
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
|
||||||
|
marginStart = (4 * dp).toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pencil.setOnClickListener {
|
||||||
|
android.widget.Toast.makeText(ctx, "Work in progress", android.widget.Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
row.addView(textCol)
|
row.addView(textCol)
|
||||||
|
row.addView(pencil)
|
||||||
row.addView(toggle)
|
row.addView(toggle)
|
||||||
container.addView(row)
|
container.addView(row)
|
||||||
p to toggle
|
p to toggle
|
||||||
@@ -328,6 +581,10 @@ class SettingsLoginsFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val toggleRows = bmlProfiles.map { p ->
|
val toggleRows = bmlProfiles.map { p ->
|
||||||
|
val avatarIv = makeCircleAvatarView(ctx, 36)
|
||||||
|
val currentBitmap = ProfileImageStore.load(ctx, ProfileImageStore.bmlKey(p.profileId))
|
||||||
|
setAvatarBitmap(avatarIv, currentBitmap)
|
||||||
|
|
||||||
val row = LinearLayout(ctx).apply {
|
val row = LinearLayout(ctx).apply {
|
||||||
orientation = LinearLayout.HORIZONTAL
|
orientation = LinearLayout.HORIZONTAL
|
||||||
gravity = Gravity.CENTER_VERTICAL
|
gravity = Gravity.CENTER_VERTICAL
|
||||||
@@ -350,8 +607,24 @@ class SettingsLoginsFragment : Fragment() {
|
|||||||
alpha = 0.6f
|
alpha = 0.6f
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
val toggle = MaterialSwitch(ctx).apply { isChecked = p.profileId !in hidden }
|
val pencil = makePencilButton(ctx)
|
||||||
|
val toggle = MaterialSwitch(ctx).apply {
|
||||||
|
isChecked = p.profileId !in hidden
|
||||||
|
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
|
||||||
|
marginStart = (4 * dp).toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val target = PendingImageTarget.Bml(p.profileId) { newBitmap ->
|
||||||
|
setAvatarBitmap(avatarIv, newBitmap)
|
||||||
|
}
|
||||||
|
pencil.setOnClickListener {
|
||||||
|
val cur = ProfileImageStore.load(ctx, ProfileImageStore.bmlKey(p.profileId))
|
||||||
|
showImagePickerMenu(pencil, target, cur)
|
||||||
|
}
|
||||||
|
val avatarSize = (36 * dp).toInt()
|
||||||
|
row.addView(avatarIv, LinearLayout.LayoutParams(avatarSize, avatarSize).apply { marginEnd = (8 * dp).toInt() })
|
||||||
row.addView(textCol)
|
row.addView(textCol)
|
||||||
|
row.addView(pencil)
|
||||||
row.addView(toggle)
|
row.addView(toggle)
|
||||||
container.addView(row)
|
container.addView(row)
|
||||||
p to toggle
|
p to toggle
|
||||||
@@ -615,6 +888,75 @@ class SettingsLoginsFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showFahipayLoginDetails(
|
||||||
|
store: CredentialStore,
|
||||||
|
loginId: String,
|
||||||
|
profile: CredentialStore.FahipayUserProfile?
|
||||||
|
) {
|
||||||
|
val ctx = requireContext()
|
||||||
|
val dp = ctx.resources.displayMetrics.density
|
||||||
|
val hide = viewModel.hideAmounts.value ?: false
|
||||||
|
val masked = "••••••"
|
||||||
|
|
||||||
|
val scroll = android.widget.ScrollView(ctx)
|
||||||
|
val container = LinearLayout(ctx).apply {
|
||||||
|
orientation = LinearLayout.VERTICAL
|
||||||
|
val pad = (16 * dp).toInt()
|
||||||
|
setPadding(pad, (8 * dp).toInt(), pad, pad)
|
||||||
|
}
|
||||||
|
scroll.addView(container)
|
||||||
|
|
||||||
|
// Avatar row with pencil
|
||||||
|
val avatarRow = LinearLayout(ctx).apply {
|
||||||
|
orientation = LinearLayout.HORIZONTAL
|
||||||
|
gravity = Gravity.CENTER_VERTICAL
|
||||||
|
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
|
||||||
|
bottomMargin = (12 * dp).toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val avatarSize = (56 * dp).toInt()
|
||||||
|
val avatarIv = makeCircleAvatarView(ctx, 56)
|
||||||
|
val currentBitmap = ProfileImageStore.load(ctx, ProfileImageStore.fahipayKey(loginId))
|
||||||
|
setAvatarBitmap(avatarIv, currentBitmap)
|
||||||
|
|
||||||
|
val pencil = makePencilButton(ctx)
|
||||||
|
val target = PendingImageTarget.Fahipay(loginId) { newBitmap ->
|
||||||
|
setAvatarBitmap(avatarIv, newBitmap)
|
||||||
|
}
|
||||||
|
pencil.setOnClickListener {
|
||||||
|
val cur = ProfileImageStore.load(ctx, ProfileImageStore.fahipayKey(loginId))
|
||||||
|
showImagePickerMenu(pencil, target, cur)
|
||||||
|
}
|
||||||
|
avatarRow.addView(avatarIv, LinearLayout.LayoutParams(avatarSize, avatarSize).apply { marginEnd = (8 * dp).toInt() })
|
||||||
|
avatarRow.addView(pencil)
|
||||||
|
container.addView(avatarRow)
|
||||||
|
|
||||||
|
// Account info lines
|
||||||
|
listOfNotNull(
|
||||||
|
profile?.fullName?.takeIf { it.isNotBlank() }?.let { "${getString(R.string.login_detail_name)}: $it" },
|
||||||
|
profile?.email?.takeIf { it.isNotBlank() }?.let { "${getString(R.string.login_detail_email)}: ${if (hide) masked else it}" },
|
||||||
|
profile?.mobile?.takeIf { it.isNotBlank() }?.let { "${getString(R.string.login_detail_mobile)}: ${if (hide) masked else it}" },
|
||||||
|
profile?.nid?.takeIf { it.isNotBlank() }?.let { "${getString(R.string.login_detail_id_card)}: ${if (hide) masked else it}" }
|
||||||
|
).forEach { line ->
|
||||||
|
container.addView(TextView(ctx).apply {
|
||||||
|
text = line
|
||||||
|
setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodyMedium)
|
||||||
|
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
|
||||||
|
bottomMargin = (4 * dp).toInt()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialAlertDialogBuilder(ctx)
|
||||||
|
.setTitle(getString(R.string.fahipay_name))
|
||||||
|
.setView(scroll)
|
||||||
|
.setPositiveButton(R.string.close, null)
|
||||||
|
.setNegativeButton(R.string.settings_logout) { _, _ ->
|
||||||
|
confirmLogout(getString(R.string.fahipay_name)) { logoutFahipay(store, loginId) }
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
private fun showLoginDetails(title: String, details: String, onLogout: () -> Unit) {
|
private fun showLoginDetails(title: String, details: String, onLogout: () -> Unit) {
|
||||||
MaterialAlertDialogBuilder(requireContext())
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
.setTitle(title)
|
.setTitle(title)
|
||||||
@@ -654,7 +996,10 @@ class SettingsLoginsFragment : Fragment() {
|
|||||||
val app = requireActivity().application as BasedBankApp
|
val app = requireActivity().application as BasedBankApp
|
||||||
// Remove all per-profile sessions for this login from the in-memory map
|
// Remove all per-profile sessions for this login from the in-memory map
|
||||||
val profiles = app.bmlProfilesMap[loginId] ?: emptyList()
|
val profiles = app.bmlProfilesMap[loginId] ?: emptyList()
|
||||||
profiles.forEach { app.bmlSessions.remove(it.profileId) }
|
profiles.forEach { p ->
|
||||||
|
app.bmlSessions.remove(p.profileId)
|
||||||
|
ProfileImageStore.delete(ctx, ProfileImageStore.bmlKey(p.profileId))
|
||||||
|
}
|
||||||
// clearBmlCredentials also clears per-profile tokens via loadBmlProfiles internally
|
// clearBmlCredentials also clears per-profile tokens via loadBmlProfiles internally
|
||||||
store.clearBmlCredentials(loginId)
|
store.clearBmlCredentials(loginId)
|
||||||
app.bmlProfilesMap.remove(loginId)
|
app.bmlProfilesMap.remove(loginId)
|
||||||
@@ -669,6 +1014,7 @@ class SettingsLoginsFragment : Fragment() {
|
|||||||
|
|
||||||
private fun logoutFahipay(store: CredentialStore, loginId: String) {
|
private fun logoutFahipay(store: CredentialStore, loginId: String) {
|
||||||
val ctx = requireContext()
|
val ctx = requireContext()
|
||||||
|
ProfileImageStore.delete(ctx, ProfileImageStore.fahipayKey(loginId))
|
||||||
store.clearFahipayCredentials(loginId)
|
store.clearFahipayCredentials(loginId)
|
||||||
val app = requireActivity().application as BasedBankApp
|
val app = requireActivity().application as BasedBankApp
|
||||||
app.fahipaySessions.remove(loginId)
|
app.fahipaySessions.remove(loginId)
|
||||||
@@ -682,5 +1028,6 @@ class SettingsLoginsFragment : Fragment() {
|
|||||||
AccountCache.clear(ctx); ContactsCache.clear(ctx); FinancingCache.clear(ctx)
|
AccountCache.clear(ctx); ContactsCache.clear(ctx); FinancingCache.clear(ctx)
|
||||||
ForeignLimitsCache.clear(ctx); RecentsCache.clear(ctx)
|
ForeignLimitsCache.clear(ctx); RecentsCache.clear(ctx)
|
||||||
CardsCache.clear(ctx); TransactionCache.clearAll(ctx); ContactImageCache.clearAll(ctx)
|
CardsCache.clear(ctx); TransactionCache.clearAll(ctx); ContactImageCache.clearAll(ctx)
|
||||||
|
// Note: ProfileImageStore is intentionally NOT cleared here — profile images are user-set data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1753,12 +1753,51 @@ class TransferFragment : Fragment() {
|
|||||||
b.ivDropdownCardLogo.visibility = View.VISIBLE
|
b.ivDropdownCardLogo.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
acc.bank == "BML" -> {
|
acc.bank == "BML" -> {
|
||||||
b.ivDropdownCardLogo.setImageResource(R.drawable.bml_logo_vector)
|
val localKey = sh.sar.basedbank.util.ProfileImageStore.bmlKey(acc.profileId)
|
||||||
b.ivDropdownCardLogo.visibility = View.VISIBLE
|
val cachedLocal = dropdownProfileImageCache[localKey]
|
||||||
|
val imageView = b.ivDropdownCardLogo
|
||||||
|
imageView.tag = localKey
|
||||||
|
if (cachedLocal != null) {
|
||||||
|
imageView.setImageBitmap(cachedLocal)
|
||||||
|
} else {
|
||||||
|
imageView.setImageResource(R.drawable.bml_logo_vector)
|
||||||
|
if (acc.profileId.isNotBlank()) {
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
val bitmap = withContext(Dispatchers.IO) {
|
||||||
|
sh.sar.basedbank.util.ProfileImageStore.load(requireContext(), localKey)
|
||||||
|
}
|
||||||
|
if (bitmap != null) {
|
||||||
|
dropdownProfileImageCache[localKey] = bitmap
|
||||||
|
if (imageView.tag == localKey) imageView.setImageBitmap(bitmap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imageView.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
acc.bank == "FAHIPAY" -> {
|
acc.bank == "FAHIPAY" -> {
|
||||||
b.ivDropdownCardLogo.setImageResource(R.drawable.fahipay_logo)
|
val loginId = sh.sar.basedbank.util.ProfileImageStore.loginIdFromTag(acc.loginTag)
|
||||||
b.ivDropdownCardLogo.visibility = View.VISIBLE
|
val localKey = sh.sar.basedbank.util.ProfileImageStore.fahipayKey(loginId)
|
||||||
|
val cachedLocal = dropdownProfileImageCache[localKey]
|
||||||
|
val imageView = b.ivDropdownCardLogo
|
||||||
|
imageView.tag = localKey
|
||||||
|
if (cachedLocal != null) {
|
||||||
|
imageView.setImageBitmap(cachedLocal)
|
||||||
|
} else {
|
||||||
|
imageView.setImageResource(R.drawable.fahipay_logo)
|
||||||
|
if (loginId.isNotBlank()) {
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
val bitmap = withContext(Dispatchers.IO) {
|
||||||
|
sh.sar.basedbank.util.ProfileImageStore.load(requireContext(), localKey)
|
||||||
|
}
|
||||||
|
if (bitmap != null) {
|
||||||
|
dropdownProfileImageCache[localKey] = bitmap
|
||||||
|
if (imageView.tag == localKey) imageView.setImageBitmap(bitmap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imageView.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
acc.bank == "MIB" -> {
|
acc.bank == "MIB" -> {
|
||||||
val hash = acc.profileImageHash
|
val hash = acc.profileImageHash
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package sh.sar.basedbank.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores and loads profile images locally for BML and Fahipay accounts.
|
||||||
|
*
|
||||||
|
* Key conventions:
|
||||||
|
* BML profile: "bml_${profileId}"
|
||||||
|
* Fahipay login: "fahipay_${loginId}"
|
||||||
|
*/
|
||||||
|
object ProfileImageStore {
|
||||||
|
|
||||||
|
private fun dir(context: Context): File =
|
||||||
|
File(context.filesDir, "profile_images").also { it.mkdirs() }
|
||||||
|
|
||||||
|
private fun file(context: Context, key: String): File =
|
||||||
|
File(dir(context), "${key.replace(Regex("[^A-Za-z0-9_\\-]"), "_")}.jpg")
|
||||||
|
|
||||||
|
fun save(context: Context, key: String, bitmap: Bitmap) {
|
||||||
|
try {
|
||||||
|
file(context, key).outputStream().use {
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, it)
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun load(context: Context, key: String): Bitmap? = try {
|
||||||
|
val f = file(context, key)
|
||||||
|
if (f.exists()) BitmapFactory.decodeFile(f.absolutePath) else null
|
||||||
|
} catch (_: Exception) { null }
|
||||||
|
|
||||||
|
fun delete(context: Context, key: String) {
|
||||||
|
try { file(context, key).delete() } catch (_: Exception) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun exists(context: Context, key: String): Boolean =
|
||||||
|
try { file(context, key).exists() } catch (_: Exception) { false }
|
||||||
|
|
||||||
|
fun clearAll(context: Context) {
|
||||||
|
try { dir(context).listFiles()?.forEach { it.delete() } } catch (_: Exception) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Key for a BML profile given its profileId. */
|
||||||
|
fun bmlKey(profileId: String) = "bml_$profileId"
|
||||||
|
|
||||||
|
/** Key for a Fahipay login given its loginId. */
|
||||||
|
fun fahipayKey(loginId: String) = "fahipay_$loginId"
|
||||||
|
|
||||||
|
/** Derives the loginId from a loginTag (e.g. "fahipay_abc" → "abc"). */
|
||||||
|
fun loginIdFromTag(loginTag: String): String =
|
||||||
|
loginTag.substringAfter('_', loginTag)
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M12,15.2c1.767,0 3.2,-1.433 3.2,-3.2s-1.433,-3.2 -3.2,-3.2 -3.2,1.433 -3.2,3.2 1.433,3.2 3.2,3.2zM9,2L7.17,4H4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V6c0,-1.1 -0.9,-2 -2,-2h-3.17L15,2H9zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5z"/>
|
||||||
|
</vector>
|
||||||
@@ -189,6 +189,13 @@
|
|||||||
<string name="login_detail_customer_id">Customer ID</string>
|
<string name="login_detail_customer_id">Customer ID</string>
|
||||||
<string name="login_detail_id_card">ID Card</string>
|
<string name="login_detail_id_card">ID Card</string>
|
||||||
<string name="login_detail_profiles">Profiles</string>
|
<string name="login_detail_profiles">Profiles</string>
|
||||||
|
<string name="profile_image_title">Profile photo</string>
|
||||||
|
<string name="profile_image_select">Select image</string>
|
||||||
|
<string name="profile_image_camera">Take from camera</string>
|
||||||
|
<string name="profile_image_remove">Remove</string>
|
||||||
|
<string name="profile_image_uploading">Uploading image…</string>
|
||||||
|
<string name="profile_image_upload_failed">Failed to upload image</string>
|
||||||
|
<string name="profile_image_deleting">Removing image…</string>
|
||||||
<string name="close">Close</string>
|
<string name="close">Close</string>
|
||||||
<string name="save">Save</string>
|
<string name="save">Save</string>
|
||||||
<string name="cancel">Cancel</string>
|
<string name="cancel">Cancel</string>
|
||||||
|
|||||||
@@ -2,4 +2,5 @@
|
|||||||
<paths>
|
<paths>
|
||||||
<cache-path name="receipt_cache" path="receipts/" />
|
<cache-path name="receipt_cache" path="receipts/" />
|
||||||
<cache-path name="qr_cache" path="qr/" />
|
<cache-path name="qr_cache" path="qr/" />
|
||||||
|
<cache-path name="profile_photo_tmp" path="." />
|
||||||
</paths>
|
</paths>
|
||||||
|
|||||||
Reference in New Issue
Block a user