From ecbbb3ed6bced99328eba0b711c7fc1a4d503d9b Mon Sep 17 00:00:00 2001 From: Shihaam Abdul Rahman Date: Sun, 17 May 2026 20:09:09 +0500 Subject: [PATCH] categorize settings to unclutter it.. might need to revert this later --- .../ui/home/SettingsAppearanceFragment.kt | 75 ++++ .../sar/basedbank/ui/home/SettingsFragment.kt | 357 ++---------------- .../ui/home/SettingsLoginsFragment.kt | 211 +++++++++++ .../ui/home/SettingsSecurityFragment.kt | 91 +++++ .../ui/home/SettingsStorageFragment.kt | 53 +++ .../res/drawable/ic_settings_appearance.xml | 10 + .../main/res/drawable/ic_settings_storage.xml | 10 + app/src/main/res/layout/fragment_settings.xml | 307 +-------------- .../layout/fragment_settings_appearance.xml | 123 ++++++ .../res/layout/fragment_settings_logins.xml | 39 ++ .../res/layout/fragment_settings_security.xml | 159 ++++++++ .../res/layout/fragment_settings_storage.xml | 30 ++ app/src/main/res/values/strings.xml | 3 + 13 files changed, 832 insertions(+), 636 deletions(-) create mode 100644 app/src/main/java/sh/sar/basedbank/ui/home/SettingsAppearanceFragment.kt create mode 100644 app/src/main/java/sh/sar/basedbank/ui/home/SettingsLoginsFragment.kt create mode 100644 app/src/main/java/sh/sar/basedbank/ui/home/SettingsSecurityFragment.kt create mode 100644 app/src/main/java/sh/sar/basedbank/ui/home/SettingsStorageFragment.kt create mode 100644 app/src/main/res/drawable/ic_settings_appearance.xml create mode 100644 app/src/main/res/drawable/ic_settings_storage.xml create mode 100644 app/src/main/res/layout/fragment_settings_appearance.xml create mode 100644 app/src/main/res/layout/fragment_settings_logins.xml create mode 100644 app/src/main/res/layout/fragment_settings_security.xml create mode 100644 app/src/main/res/layout/fragment_settings_storage.xml diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/SettingsAppearanceFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/SettingsAppearanceFragment.kt new file mode 100644 index 0000000..5f4c2da --- /dev/null +++ b/app/src/main/java/sh/sar/basedbank/ui/home/SettingsAppearanceFragment.kt @@ -0,0 +1,75 @@ +package sh.sar.basedbank.ui.home + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatDelegate +import androidx.appcompat.app.AppCompatDelegate.setApplicationLocales +import androidx.core.os.LocaleListCompat +import androidx.fragment.app.Fragment +import sh.sar.basedbank.R +import sh.sar.basedbank.databinding.FragmentSettingsAppearanceBinding + +class SettingsAppearanceFragment : Fragment() { + + private var _binding: FragmentSettingsAppearanceBinding? = null + private val binding get() = _binding!! + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + _binding = FragmentSettingsAppearanceBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val prefs = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE) + + // Navigation mode + val isBottom = prefs.getBoolean("bottom_nav", false) + binding.navModeToggle.check(if (isBottom) R.id.btnNavBottom else R.id.btnNavDrawer) + binding.navModeToggle.addOnButtonCheckedListener { _, checkedId, isChecked -> + if (!isChecked) return@addOnButtonCheckedListener + prefs.edit().putBoolean("bottom_nav", checkedId == R.id.btnNavBottom).apply() + (activity as? HomeActivity)?.applyNavMode() + } + + // Theme + val saved = prefs.getString("theme", "system") + binding.themeToggle.check(when (saved) { + "light" -> R.id.btnThemeLight + "dark" -> R.id.btnThemeDark + else -> R.id.btnThemeSystem + }) + binding.themeToggle.addOnButtonCheckedListener { _, checkedId, isChecked -> + if (!isChecked) return@addOnButtonCheckedListener + val (key, mode) = when (checkedId) { + R.id.btnThemeLight -> "light" to AppCompatDelegate.MODE_NIGHT_NO + R.id.btnThemeDark -> "dark" to AppCompatDelegate.MODE_NIGHT_YES + else -> "system" to AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM + } + prefs.edit().putString("theme", key).apply() + AppCompatDelegate.setDefaultNightMode(mode) + } + + // Language + val currentLocales = AppCompatDelegate.getApplicationLocales() + val currentLang = if (currentLocales.isEmpty) "en" else currentLocales[0]?.language ?: "en" + binding.languageToggle.check(if (currentLang == "dv") R.id.btnLangDhivehi else R.id.btnLangEnglish) + binding.languageToggle.addOnButtonCheckedListener { _, checkedId, isChecked -> + if (!isChecked) return@addOnButtonCheckedListener + val tag = if (checkedId == R.id.btnLangDhivehi) "dv" else "en" + setApplicationLocales(LocaleListCompat.forLanguageTags(tag)) + } + } + + override fun onResume() { + super.onResume() + requireActivity().title = getString(R.string.settings_appearance) + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/SettingsFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/SettingsFragment.kt index b70fe67..fa3de55 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/SettingsFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/SettingsFragment.kt @@ -1,358 +1,51 @@ package sh.sar.basedbank.ui.home -import android.content.Context import android.os.Bundle -import android.view.Gravity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView -import android.widget.Toast -import androidx.appcompat.app.AppCompatDelegate -import androidx.appcompat.app.AppCompatDelegate.setApplicationLocales -import androidx.biometric.BiometricManager -import androidx.core.os.LocaleListCompat +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes import androidx.fragment.app.Fragment -import android.content.Intent -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import sh.sar.basedbank.BasedBankApp import sh.sar.basedbank.R -import sh.sar.basedbank.api.mib.TransactionCache -import sh.sar.basedbank.databinding.FragmentSettingsBinding -import sh.sar.basedbank.ui.onboarding.SecuritySetupFragment -import sh.sar.basedbank.util.AccountCache -import sh.sar.basedbank.util.ContactImageCache -import sh.sar.basedbank.util.ContactsCache -import sh.sar.basedbank.util.CredentialStore -import sh.sar.basedbank.ui.login.LoginActivity -import sh.sar.basedbank.util.FinancingCache -import sh.sar.basedbank.util.ForeignLimitsCache -import sh.sar.basedbank.util.RecentsCache class SettingsFragment : Fragment() { - private var _binding: FragmentSettingsBinding? = null - private val binding get() = _binding!! + private data class SettingsItem( + @DrawableRes val icon: Int, + @StringRes val title: Int, + val dest: () -> Fragment + ) - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - _binding = FragmentSettingsBinding.inflate(inflater, container, false) - return binding.root - } + private val items = listOf( + SettingsItem(R.drawable.ic_contacts, R.string.settings_logins) { SettingsLoginsFragment() }, + SettingsItem(R.drawable.ic_settings_appearance, R.string.settings_appearance) { SettingsAppearanceFragment() }, + SettingsItem(R.drawable.ic_lock, R.string.settings_privacy_security) { SettingsSecurityFragment() }, + SettingsItem(R.drawable.ic_settings_storage, R.string.settings_storage) { SettingsStorageFragment() }, + ) + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = + inflater.inflate(R.layout.fragment_settings, container, false) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - val prefs = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE) - - // Navigation mode - val isBottom = prefs.getBoolean("bottom_nav", false) - binding.navModeToggle.check(if (isBottom) R.id.btnNavBottom else R.id.btnNavDrawer) - binding.navModeToggle.addOnButtonCheckedListener { _, checkedId, isChecked -> - if (!isChecked) return@addOnButtonCheckedListener - prefs.edit().putBoolean("bottom_nav", checkedId == R.id.btnNavBottom).apply() - (activity as? HomeActivity)?.applyNavMode() - } - - // Theme - val saved = prefs.getString("theme", "system") - val initialId = when (saved) { - "light" -> R.id.btnThemeLight - "dark" -> R.id.btnThemeDark - else -> R.id.btnThemeSystem - } - binding.themeToggle.check(initialId) - - binding.themeToggle.addOnButtonCheckedListener { _, checkedId, isChecked -> - if (!isChecked) return@addOnButtonCheckedListener - val (key, mode) = when (checkedId) { - R.id.btnThemeLight -> "light" to AppCompatDelegate.MODE_NIGHT_NO - R.id.btnThemeDark -> "dark" to AppCompatDelegate.MODE_NIGHT_YES - else -> "system" to AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM + val list = view.findViewById(R.id.settingsList) + val inflater = LayoutInflater.from(requireContext()) + for (item in items) { + val row = inflater.inflate(R.layout.item_more_nav, list, false) + row.findViewById(R.id.ivIcon).setImageResource(item.icon) + row.findViewById(R.id.tvLabel).setText(item.title) + row.setOnClickListener { + (requireActivity() as HomeActivity).showWithBackStack(item.dest()) } - prefs.edit().putString("theme", key).apply() - AppCompatDelegate.setDefaultNightMode(mode) - } - - // Language - val currentLocales = AppCompatDelegate.getApplicationLocales() - val currentLang = if (currentLocales.isEmpty) "en" else currentLocales[0]?.language ?: "en" - binding.languageToggle.check( - if (currentLang == "dv") R.id.btnLangDhivehi else R.id.btnLangEnglish - ) - - binding.languageToggle.addOnButtonCheckedListener { _, checkedId, isChecked -> - if (!isChecked) return@addOnButtonCheckedListener - val tag = if (checkedId == R.id.btnLangDhivehi) "dv" else "en" - setApplicationLocales(LocaleListCompat.forLanguageTags(tag)) - } - - // Auto-lock - val savedTimeout = prefs.getLong("autolock_timeout", 60_000L) - binding.autolockToggle.check(when (savedTimeout) { - 0L -> R.id.btnAutolockOff - 30_000L -> R.id.btnAutolock30s - 180_000L -> R.id.btnAutolock3m - 300_000L -> R.id.btnAutolock5m - else -> R.id.btnAutolock1m - }) - binding.autolockToggle.addOnButtonCheckedListener { _, checkedId, isChecked -> - if (!isChecked) return@addOnButtonCheckedListener - val timeout = when (checkedId) { - R.id.btnAutolockOff -> 0L - R.id.btnAutolock30s -> 30_000L - R.id.btnAutolock3m -> 180_000L - R.id.btnAutolock5m -> 300_000L - else -> 60_000L - } - prefs.edit().putLong("autolock_timeout", timeout).apply() - (activity as? HomeActivity)?.resetAutolockTimer() - } - - // Change lock - binding.btnChangeLock.setOnClickListener { - (requireActivity() as HomeActivity).showWithBackStack( - SecuritySetupFragment.newInstance(changeMode = true) - ) - } - - // Biometrics toggle — only show if device supports it - val canUseBiometrics = BiometricManager.from(requireContext()) - .canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) == BiometricManager.BIOMETRIC_SUCCESS - if (canUseBiometrics) { - binding.rowBiometrics.visibility = View.VISIBLE - binding.switchBiometrics.setOnCheckedChangeListener { _, isChecked -> - prefs.edit().putBoolean("biometrics_enabled", isChecked).apply() - } - } - - // Block screenshots - val blockScreenshots = prefs.getBoolean("block_screenshots", true) - binding.switchBlockScreenshots.isChecked = blockScreenshots - applyFlagSecure(blockScreenshots) - binding.switchBlockScreenshots.setOnCheckedChangeListener { _, isChecked -> - prefs.edit().putBoolean("block_screenshots", isChecked).apply() - applyFlagSecure(isChecked) - } - - // Add account - binding.btnAddAccount.setOnClickListener { - startActivity(Intent(requireContext(), LoginActivity::class.java)) - } - - // Clear cache - binding.btnClearCache.setOnClickListener { - val ctx = requireContext() - clearAllCaches(ctx) - Toast.makeText(ctx, R.string.settings_cache_cleared, Toast.LENGTH_SHORT).show() + list.addView(row) } } override fun onResume() { super.onResume() requireActivity().title = getString(R.string.nav_settings) - val prefs = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE) - if (binding.rowBiometrics.visibility == View.VISIBLE) { - binding.switchBiometrics.isChecked = prefs.getBoolean("biometrics_enabled", false) - } - buildLoginsSection() - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - - // ── Logins section ─────────────────────────────────────────────────────── - - private fun buildLoginsSection() { - val ctx = requireContext() - val store = CredentialStore(ctx) - val container = binding.loginsContainer - container.removeAllViews() - - val hasMib = store.hasMibCredentials() - val hasBml = store.hasBmlCredentials() - val hasFahipay = store.hasFahipayCredentials() - - binding.tvLoginsTitle.visibility = if (hasMib || hasBml || hasFahipay) View.VISIBLE else View.GONE - - if (hasMib) { - val profile = store.loadMibUserProfile() - val displayName = profile?.fullName?.takeIf { it.isNotBlank() } ?: getString(R.string.mib_name) - val profileNames = AccountCache.load(ctx) - .map { it.profileName }.filter { it.isNotBlank() }.distinct() - addLoginRow(container, R.drawable.mib_logo, displayName) { - showLoginDetails( - title = getString(R.string.mib_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)}: ${profile!!.email}") - if (!profile?.mobile.isNullOrBlank()) appendLine("${getString(R.string.login_detail_mobile)}: ${profile!!.mobile}") - if (profileNames.isNotEmpty()) { - appendLine() - appendLine(getString(R.string.login_detail_profiles)) - profileNames.forEach { appendLine(" • $it") } - } - }.trim(), - onLogout = { confirmLogout(getString(R.string.mib_name)) { logoutMib(store) } } - ) - } - } - - if (hasBml) { - val profile = store.loadBmlUserProfile() - val displayName = profile?.fullName?.takeIf { it.isNotBlank() } ?: getString(R.string.bml_name) - val profileNames = AccountCache.loadBml(ctx) - .map { it.profileName }.filter { it.isNotBlank() }.distinct() - addLoginRow(container, R.drawable.bml_logo_vector, displayName) { - showLoginDetails( - title = getString(R.string.bml_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)}: ${profile!!.email}") - if (!profile?.mobile.isNullOrBlank()) appendLine("${getString(R.string.login_detail_mobile)}: ${profile!!.mobile}") - if (!profile?.customerId.isNullOrBlank()) appendLine("${getString(R.string.login_detail_customer_id)}: ${profile!!.customerId}") - if (!profile?.idCard.isNullOrBlank()) appendLine("${getString(R.string.login_detail_id_card)}: ${profile!!.idCard}") - if (profileNames.isNotEmpty()) { - appendLine() - appendLine(getString(R.string.login_detail_profiles)) - profileNames.forEach { appendLine(" • $it") } - } - }.trim(), - onLogout = { confirmLogout(getString(R.string.bml_name)) { logoutBml(store) } } - ) - } - } - - if (hasFahipay) { - val profile = store.loadFahipayUserProfile() - val displayName = profile?.fullName?.takeIf { it.isNotBlank() } ?: getString(R.string.fahipay_name) - addLoginRow(container, R.drawable.fahipay_logo, displayName) { - 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)}: ${profile!!.email}") - if (!profile?.mobile.isNullOrBlank()) appendLine("${getString(R.string.login_detail_mobile)}: ${profile!!.mobile}") - if (!profile?.nid.isNullOrBlank()) appendLine("${getString(R.string.login_detail_id_card)}: ${profile!!.nid}") - }.trim(), - onLogout = { confirmLogout(getString(R.string.fahipay_name)) { logoutFahipay(store) } } - ) - } - } - } - - private fun addLoginRow( - container: LinearLayout, - logoRes: Int, - displayName: String, - onClick: () -> Unit - ) { - val ctx = requireContext() - val dp = ctx.resources.displayMetrics.density - - val row = LinearLayout(ctx).apply { - orientation = LinearLayout.HORIZONTAL - gravity = Gravity.CENTER_VERTICAL - setPadding(0, (12 * dp).toInt(), 0, (12 * dp).toInt()) - isClickable = true - isFocusable = true - val ta = ctx.obtainStyledAttributes(intArrayOf(android.R.attr.selectableItemBackground)) - background = ta.getDrawable(0) - ta.recycle() - setOnClickListener { onClick() } - } - - val logo = ImageView(ctx).apply { - setImageResource(logoRes) - scaleType = ImageView.ScaleType.CENTER_INSIDE - layoutParams = LinearLayout.LayoutParams((36 * dp).toInt(), (36 * dp).toInt()).apply { - marginEnd = (12 * dp).toInt() - } - } - - val tvName = TextView(ctx).apply { - text = displayName - setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodyLarge) - layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f) - } - - row.addView(logo) - row.addView(tvName) - container.addView(row) - } - - private fun showLoginDetails(title: String, details: String, onLogout: () -> Unit) { - MaterialAlertDialogBuilder(requireContext()) - .setTitle(title) - .apply { if (details.isNotBlank()) setMessage(details) } - .setPositiveButton(R.string.close, null) - .setNegativeButton(R.string.settings_logout) { _, _ -> onLogout() } - .show() - } - - private fun confirmLogout(bankName: String, onConfirm: () -> Unit) { - MaterialAlertDialogBuilder(requireContext()) - .setTitle(getString(R.string.settings_logout_confirm_title, bankName)) - .setMessage(R.string.settings_logout_confirm_message) - .setPositiveButton(R.string.settings_logout) { _, _ -> onConfirm() } - .setNegativeButton(R.string.cancel, null) - .show() - } - - private fun logoutMib(store: CredentialStore) { - val ctx = requireContext() - store.clearMibCredentials() - ctx.getSharedPreferences("mib_prefs", Context.MODE_PRIVATE).edit().clear().apply() - val app = requireActivity().application as BasedBankApp - app.accounts = emptyList() - app.mibSession = null - app.mibProfiles = emptyList() - clearAllCaches(ctx) - (activity as HomeActivity).relogin() - buildLoginsSection() - } - - private fun logoutBml(store: CredentialStore) { - val ctx = requireContext() - store.clearBmlCredentials() - store.clearBmlSession() - val app = requireActivity().application as BasedBankApp - app.bmlSession = null - app.bmlAccounts = emptyList() - clearAllCaches(ctx) - (activity as HomeActivity).relogin() - buildLoginsSection() - } - - private fun logoutFahipay(store: CredentialStore) { - val ctx = requireContext() - store.clearFahipayCredentials() - store.clearFahipaySession() - val app = requireActivity().application as BasedBankApp - app.fahipaySession = null - app.fahipayAccounts = emptyList() - clearAllCaches(ctx) - (activity as HomeActivity).relogin() - buildLoginsSection() - } - - private fun applyFlagSecure(enabled: Boolean) { - val win = activity?.window ?: return - if (enabled) { - win.addFlags(android.view.WindowManager.LayoutParams.FLAG_SECURE) - } else { - win.clearFlags(android.view.WindowManager.LayoutParams.FLAG_SECURE) - } - } - - private fun clearAllCaches(ctx: Context) { - AccountCache.clear(ctx) - ContactsCache.clear(ctx) - FinancingCache.clear(ctx) - ForeignLimitsCache.clear(ctx) - RecentsCache.clear(ctx) - TransactionCache.clearAll(ctx) - ContactImageCache.clearAll(ctx) } } diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/SettingsLoginsFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/SettingsLoginsFragment.kt new file mode 100644 index 0000000..f147d57 --- /dev/null +++ b/app/src/main/java/sh/sar/basedbank/ui/home/SettingsLoginsFragment.kt @@ -0,0 +1,211 @@ +package sh.sar.basedbank.ui.home + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import androidx.fragment.app.Fragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import sh.sar.basedbank.BasedBankApp +import sh.sar.basedbank.R +import sh.sar.basedbank.api.mib.TransactionCache +import sh.sar.basedbank.databinding.FragmentSettingsLoginsBinding +import sh.sar.basedbank.ui.login.LoginActivity +import sh.sar.basedbank.util.AccountCache +import sh.sar.basedbank.util.ContactImageCache +import sh.sar.basedbank.util.ContactsCache +import sh.sar.basedbank.util.CredentialStore +import sh.sar.basedbank.util.FinancingCache +import sh.sar.basedbank.util.ForeignLimitsCache +import sh.sar.basedbank.util.RecentsCache + +class SettingsLoginsFragment : Fragment() { + + private var _binding: FragmentSettingsLoginsBinding? = null + private val binding get() = _binding!! + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + _binding = FragmentSettingsLoginsBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + binding.btnAddAccount.setOnClickListener { + startActivity(Intent(requireContext(), LoginActivity::class.java)) + } + } + + override fun onResume() { + super.onResume() + requireActivity().title = getString(R.string.settings_logins) + buildLoginsSection() + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + private fun buildLoginsSection() { + val ctx = requireContext() + val store = CredentialStore(ctx) + val container = binding.loginsContainer + container.removeAllViews() + + val hasMib = store.hasMibCredentials() + val hasBml = store.hasBmlCredentials() + val hasFahipay = store.hasFahipayCredentials() + + binding.tvLoginsTitle.visibility = if (hasMib || hasBml || hasFahipay) View.VISIBLE else View.GONE + + if (hasMib) { + val profile = store.loadMibUserProfile() + val displayName = profile?.fullName?.takeIf { it.isNotBlank() } ?: getString(R.string.mib_name) + val profileNames = AccountCache.load(ctx).map { it.profileName }.filter { it.isNotBlank() }.distinct() + addLoginRow(container, R.drawable.mib_logo, displayName) { + showLoginDetails( + title = getString(R.string.mib_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)}: ${profile!!.email}") + if (!profile?.mobile.isNullOrBlank()) appendLine("${getString(R.string.login_detail_mobile)}: ${profile!!.mobile}") + if (profileNames.isNotEmpty()) { + appendLine() + appendLine(getString(R.string.login_detail_profiles)) + profileNames.forEach { appendLine(" • $it") } + } + }.trim(), + onLogout = { confirmLogout(getString(R.string.mib_name)) { logoutMib(store) } } + ) + } + } + + if (hasBml) { + val profile = store.loadBmlUserProfile() + val displayName = profile?.fullName?.takeIf { it.isNotBlank() } ?: getString(R.string.bml_name) + val profileNames = AccountCache.loadBml(ctx).map { it.profileName }.filter { it.isNotBlank() }.distinct() + addLoginRow(container, R.drawable.bml_logo_vector, displayName) { + showLoginDetails( + title = getString(R.string.bml_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)}: ${profile!!.email}") + if (!profile?.mobile.isNullOrBlank()) appendLine("${getString(R.string.login_detail_mobile)}: ${profile!!.mobile}") + if (!profile?.customerId.isNullOrBlank()) appendLine("${getString(R.string.login_detail_customer_id)}: ${profile!!.customerId}") + if (!profile?.idCard.isNullOrBlank()) appendLine("${getString(R.string.login_detail_id_card)}: ${profile!!.idCard}") + if (profileNames.isNotEmpty()) { + appendLine() + appendLine(getString(R.string.login_detail_profiles)) + profileNames.forEach { appendLine(" • $it") } + } + }.trim(), + onLogout = { confirmLogout(getString(R.string.bml_name)) { logoutBml(store) } } + ) + } + } + + if (hasFahipay) { + val profile = store.loadFahipayUserProfile() + val displayName = profile?.fullName?.takeIf { it.isNotBlank() } ?: getString(R.string.fahipay_name) + addLoginRow(container, R.drawable.fahipay_logo, displayName) { + 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)}: ${profile!!.email}") + if (!profile?.mobile.isNullOrBlank()) appendLine("${getString(R.string.login_detail_mobile)}: ${profile!!.mobile}") + if (!profile?.nid.isNullOrBlank()) appendLine("${getString(R.string.login_detail_id_card)}: ${profile!!.nid}") + }.trim(), + onLogout = { confirmLogout(getString(R.string.fahipay_name)) { logoutFahipay(store) } } + ) + } + } + } + + private fun addLoginRow(container: LinearLayout, logoRes: Int, displayName: String, onClick: () -> Unit) { + val ctx = requireContext() + val dp = ctx.resources.displayMetrics.density + val row = LinearLayout(ctx).apply { + orientation = LinearLayout.HORIZONTAL + gravity = Gravity.CENTER_VERTICAL + setPadding(0, (12 * dp).toInt(), 0, (12 * dp).toInt()) + isClickable = true; isFocusable = true + val ta = ctx.obtainStyledAttributes(intArrayOf(android.R.attr.selectableItemBackground)) + background = ta.getDrawable(0); ta.recycle() + setOnClickListener { onClick() } + } + val logo = ImageView(ctx).apply { + setImageResource(logoRes) + scaleType = ImageView.ScaleType.CENTER_INSIDE + layoutParams = LinearLayout.LayoutParams((36 * dp).toInt(), (36 * dp).toInt()).apply { marginEnd = (12 * dp).toInt() } + } + val tvName = TextView(ctx).apply { + text = displayName + setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodyLarge) + layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f) + } + row.addView(logo); row.addView(tvName) + container.addView(row) + } + + private fun showLoginDetails(title: String, details: String, onLogout: () -> Unit) { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(title) + .apply { if (details.isNotBlank()) setMessage(details) } + .setPositiveButton(R.string.close, null) + .setNegativeButton(R.string.settings_logout) { _, _ -> onLogout() } + .show() + } + + private fun confirmLogout(bankName: String, onConfirm: () -> Unit) { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(getString(R.string.settings_logout_confirm_title, bankName)) + .setMessage(R.string.settings_logout_confirm_message) + .setPositiveButton(R.string.settings_logout) { _, _ -> onConfirm() } + .setNegativeButton(R.string.cancel, null) + .show() + } + + private fun logoutMib(store: CredentialStore) { + val ctx = requireContext() + store.clearMibCredentials() + ctx.getSharedPreferences("mib_prefs", Context.MODE_PRIVATE).edit().clear().apply() + val app = requireActivity().application as BasedBankApp + app.accounts = emptyList(); app.mibSession = null; app.mibProfiles = emptyList() + clearAllCaches(ctx) + (activity as HomeActivity).relogin() + buildLoginsSection() + } + + private fun logoutBml(store: CredentialStore) { + val ctx = requireContext() + store.clearBmlCredentials(); store.clearBmlSession() + val app = requireActivity().application as BasedBankApp + app.bmlSession = null; app.bmlAccounts = emptyList() + clearAllCaches(ctx) + (activity as HomeActivity).relogin() + buildLoginsSection() + } + + private fun logoutFahipay(store: CredentialStore) { + val ctx = requireContext() + store.clearFahipayCredentials(); store.clearFahipaySession() + val app = requireActivity().application as BasedBankApp + app.fahipaySession = null; app.fahipayAccounts = emptyList() + clearAllCaches(ctx) + (activity as HomeActivity).relogin() + buildLoginsSection() + } + + private fun clearAllCaches(ctx: Context) { + AccountCache.clear(ctx); ContactsCache.clear(ctx); FinancingCache.clear(ctx) + ForeignLimitsCache.clear(ctx); RecentsCache.clear(ctx) + TransactionCache.clearAll(ctx); ContactImageCache.clearAll(ctx) + } +} diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/SettingsSecurityFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/SettingsSecurityFragment.kt new file mode 100644 index 0000000..e568108 --- /dev/null +++ b/app/src/main/java/sh/sar/basedbank/ui/home/SettingsSecurityFragment.kt @@ -0,0 +1,91 @@ +package sh.sar.basedbank.ui.home + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.biometric.BiometricManager +import androidx.fragment.app.Fragment +import sh.sar.basedbank.R +import sh.sar.basedbank.databinding.FragmentSettingsSecurityBinding +import sh.sar.basedbank.ui.onboarding.SecuritySetupFragment + +class SettingsSecurityFragment : Fragment() { + + private var _binding: FragmentSettingsSecurityBinding? = null + private val binding get() = _binding!! + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + _binding = FragmentSettingsSecurityBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val prefs = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE) + + // Change lock + binding.btnChangeLock.setOnClickListener { + (requireActivity() as HomeActivity).showWithBackStack( + SecuritySetupFragment.newInstance(changeMode = true) + ) + } + + // Biometrics + val canUseBiometrics = BiometricManager.from(requireContext()) + .canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) == BiometricManager.BIOMETRIC_SUCCESS + if (canUseBiometrics) { + binding.rowBiometrics.visibility = View.VISIBLE + binding.switchBiometrics.isChecked = prefs.getBoolean("biometrics_enabled", false) + binding.switchBiometrics.setOnCheckedChangeListener { _, isChecked -> + prefs.edit().putBoolean("biometrics_enabled", isChecked).apply() + } + } + + // Auto-lock + binding.autolockToggle.check(when (prefs.getLong("autolock_timeout", 60_000L)) { + 0L -> R.id.btnAutolockOff + 30_000L -> R.id.btnAutolock30s + 180_000L -> R.id.btnAutolock3m + 300_000L -> R.id.btnAutolock5m + else -> R.id.btnAutolock1m + }) + binding.autolockToggle.addOnButtonCheckedListener { _, checkedId, isChecked -> + if (!isChecked) return@addOnButtonCheckedListener + val timeout = when (checkedId) { + R.id.btnAutolockOff -> 0L + R.id.btnAutolock30s -> 30_000L + R.id.btnAutolock3m -> 180_000L + R.id.btnAutolock5m -> 300_000L + else -> 60_000L + } + prefs.edit().putLong("autolock_timeout", timeout).apply() + (activity as? HomeActivity)?.resetAutolockTimer() + } + + // Block screenshots + val blockScreenshots = prefs.getBoolean("block_screenshots", true) + binding.switchBlockScreenshots.isChecked = blockScreenshots + applyFlagSecure(blockScreenshots) + binding.switchBlockScreenshots.setOnCheckedChangeListener { _, isChecked -> + prefs.edit().putBoolean("block_screenshots", isChecked).apply() + applyFlagSecure(isChecked) + } + } + + override fun onResume() { + super.onResume() + requireActivity().title = getString(R.string.settings_privacy_security) + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + private fun applyFlagSecure(enabled: Boolean) { + val win = activity?.window ?: return + if (enabled) win.addFlags(android.view.WindowManager.LayoutParams.FLAG_SECURE) + else win.clearFlags(android.view.WindowManager.LayoutParams.FLAG_SECURE) + } +} diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/SettingsStorageFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/SettingsStorageFragment.kt new file mode 100644 index 0000000..c24b359 --- /dev/null +++ b/app/src/main/java/sh/sar/basedbank/ui/home/SettingsStorageFragment.kt @@ -0,0 +1,53 @@ +package sh.sar.basedbank.ui.home + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.fragment.app.Fragment +import sh.sar.basedbank.R +import sh.sar.basedbank.api.mib.TransactionCache +import sh.sar.basedbank.databinding.FragmentSettingsStorageBinding +import sh.sar.basedbank.util.AccountCache +import sh.sar.basedbank.util.ContactImageCache +import sh.sar.basedbank.util.ContactsCache +import sh.sar.basedbank.util.FinancingCache +import sh.sar.basedbank.util.ForeignLimitsCache +import sh.sar.basedbank.util.RecentsCache + +class SettingsStorageFragment : Fragment() { + + private var _binding: FragmentSettingsStorageBinding? = null + private val binding get() = _binding!! + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + _binding = FragmentSettingsStorageBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + binding.btnClearCache.setOnClickListener { + val ctx = requireContext() + clearAllCaches(ctx) + Toast.makeText(ctx, R.string.settings_cache_cleared, Toast.LENGTH_SHORT).show() + } + } + + override fun onResume() { + super.onResume() + requireActivity().title = getString(R.string.settings_storage) + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + private fun clearAllCaches(ctx: Context) { + AccountCache.clear(ctx); ContactsCache.clear(ctx); FinancingCache.clear(ctx) + ForeignLimitsCache.clear(ctx); RecentsCache.clear(ctx) + TransactionCache.clearAll(ctx); ContactImageCache.clearAll(ctx) + } +} diff --git a/app/src/main/res/drawable/ic_settings_appearance.xml b/app/src/main/res/drawable/ic_settings_appearance.xml new file mode 100644 index 0000000..c062165 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_appearance.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_settings_storage.xml b/app/src/main/res/drawable/ic_settings_storage.xml new file mode 100644 index 0000000..921e030 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_storage.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml index 020f97e..be97e89 100644 --- a/app/src/main/res/layout/fragment_settings.xml +++ b/app/src/main/res/layout/fragment_settings.xml @@ -1,317 +1,16 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:paddingTop="8dp" + android:paddingBottom="8dp" /> diff --git a/app/src/main/res/layout/fragment_settings_appearance.xml b/app/src/main/res/layout/fragment_settings_appearance.xml new file mode 100644 index 0000000..e29467b --- /dev/null +++ b/app/src/main/res/layout/fragment_settings_appearance.xml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_settings_logins.xml b/app/src/main/res/layout/fragment_settings_logins.xml new file mode 100644 index 0000000..0e73fd1 --- /dev/null +++ b/app/src/main/res/layout/fragment_settings_logins.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_settings_security.xml b/app/src/main/res/layout/fragment_settings_security.xml new file mode 100644 index 0000000..3fce8fd --- /dev/null +++ b/app/src/main/res/layout/fragment_settings_security.xml @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_settings_storage.xml b/app/src/main/res/layout/fragment_settings_storage.xml new file mode 100644 index 0000000..d0ab083 --- /dev/null +++ b/app/src/main/res/layout/fragment_settings_storage.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 81c68d0..dfe9e4a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -126,6 +126,9 @@ Navigation Drawer Bottom Bar + Appearance + Privacy & Security + Storage Logins Log out Log out of %s?