From 7e52b510a1d9f004a2306ff3c218279ea7d49f9b Mon Sep 17 00:00:00 2001 From: Shihaam Abdul Rahman Date: Fri, 25 Sep 2026 04:20:40 +0500 Subject: [PATCH] disable unnessary api requests on saving profile/account disable and also disable all accounts for a profile when that profile is disabled --- .../sh/sar/basedbank/ui/home/HomeActivity.kt | 193 +++++++++++++----- .../ui/home/SettingsLoginsFragment.kt | 46 ++++- 2 files changed, 183 insertions(+), 56 deletions(-) diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt b/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt index 8288145..947b654 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt @@ -244,9 +244,9 @@ class HomeActivity : AppCompatActivity() { val cachedFahipay = AccountCache.loadFahipay(this, store.getFahipayLoginIds()) val cachedMfaisa = AccountCache.loadMfaisa(this, store.getMfaisaLoginIds()) val merged = cachedMib + cachedBml + cachedFahipay + cachedMfaisa - if (merged.isNotEmpty()) viewModel.accounts.value = merged + if (merged.isNotEmpty()) viewModel.accounts.value = merged.filterVisibleAccounts() val cachedCards = CardsCache.load(this) - if (cachedCards.isNotEmpty()) viewModel.mibCards.value = cachedCards + if (cachedCards.isNotEmpty()) viewModel.mibCards.value = cachedCards.filterVisibleCards() val cachedFinancing = FinancingCache.load(this) if (cachedFinancing.isNotEmpty()) viewModel.financing.value = cachedFinancing val cachedBmlLoans = FinancingCache.loadBmlLoans(this) @@ -774,47 +774,13 @@ fun applyNavLabelVisibility() { if (savedProfiles.isNotEmpty()) app.bmlProfilesMap[loginId] = savedProfiles - val bmlClient = BmlAccountClient() + // Hidden profiles are never fetched; unhiding one fetches it on its own (fetchEnabledBmlProfiles) + val hiddenProfiles = store.getHiddenBmlProfileIds(loginId) for (profile in savedProfiles) { - val saved = store.loadBmlProfileSession(profile.profileId) - val refreshToken = store.loadBmlProfileRefreshToken(profile.profileId) - if (saved == null) { - allAccounts += AccountCache.loadBml(this@HomeActivity, loginId) - .filter { it.profileId == profile.profileId } - continue - } - val expiresAt = store.loadBmlProfileExpiresAt(profile.profileId) - val tokenKnownExpired = expiresAt > 0L && System.currentTimeMillis() >= expiresAt - - suspend fun fetchWithSession(session: BmlSession) { - bmlClient.checkProfile(session) - val accounts = bmlClient.fetchAccounts(session, loginTag, profile.name, profile.profileId) - app.bmlSessions[profile.profileId] = session - allAccounts += accounts - } - - suspend fun tryRefresh() { - if (refreshToken == null) throw Exception("No refresh token") - val oldSession = BmlSession(saved.first, saved.second, refreshToken) - val newSession = app.bmlFlowFor(loginId).refreshSession(oldSession) - store.saveBmlProfileSession(profile.profileId, newSession.accessToken, newSession.deviceId) - if (newSession.refreshToken.isNotBlank()) - store.saveBmlProfileRefreshToken(profile.profileId, newSession.refreshToken) - if (newSession.expiresAt > 0) - store.saveBmlProfileExpiresAt(profile.profileId, newSession.expiresAt) - fetchWithSession(newSession) - } - + if (profile.profileId in hiddenProfiles) continue try { - if (tokenKnownExpired) { - tryRefresh() - } else { - try { - fetchWithSession(BmlSession(saved.first, saved.second)) - } catch (_: AuthExpiredException) { - tryRefresh() - } - } + allAccounts += fetchBmlProfileAccounts(store, loginId, profile) + ?: AccountCache.loadBml(this@HomeActivity, loginId).filter { it.profileId == profile.profileId } } catch (e: java.io.IOException) { refreshErrors.add("NO_INTERNET") allAccounts += AccountCache.loadBml(this@HomeActivity, loginId) @@ -985,7 +951,7 @@ fun applyNavLabelVisibility() { refreshBmlLoanDetails() for ((loginId, session) in app.mibSessions) { val profiles = app.mibProfilesMap[loginId] ?: emptyList() - refreshMibCards(loginId, session, profiles) + refreshMibCards(loginId, session, profiles.filterVisibleProfiles(loginId)) } } } @@ -1024,10 +990,140 @@ fun applyNavLabelVisibility() { return filter { it.profileId !in hidden } } - /** Called by SettingsLoginsFragment after the user changes profile visibility. */ - fun applyProfileVisibility() { - val current = viewModel.accounts.value ?: return - viewModel.accounts.value = current.filterVisibleAccounts() + /** Drops MIB cards belonging to profiles the user has hidden. */ + private fun List.filterVisibleCards(): List { + val store = CredentialStore(this@HomeActivity) + return filter { card -> + card.profileId.isEmpty() || card.profileId !in store.getHiddenMibProfileIds(card.loginTag.removePrefix("mib_")) + } + } + + /** + * Called by SettingsLoginsFragment after the user hides/unhides profiles or accounts. + * Re-filters the in-memory data only — no network. Newly unhidden profiles are fetched + * separately via [fetchEnabledMibProfiles] / [fetchEnabledBmlProfiles]. + */ + fun applyVisibility() { + val app = application as BasedBankApp + viewModel.accounts.value = (app.mibAccounts + app.bmlAccounts + app.fahipayAccounts + app.mfaisaAccounts).filterVisibleAccounts() + viewModel.mibCards.value?.let { cards -> + val visible = cards.filterVisibleCards() + viewModel.mibCards.value = visible + CardsCache.save(this, visible) + } + } + + /** Fetches accounts, cards and financing for MIB profiles the user just unhid (hidden ones are skipped on refresh). */ + fun fetchEnabledMibProfiles(loginId: String, profileIds: Set) { + if (profileIds.isEmpty()) return + val app = application as BasedBankApp + val loginTag = "mib_$loginId" + val enabled = (app.mibProfilesMap[loginId] ?: emptyList()).filter { it.profileId in profileIds } + binding.refreshIndicator.visibility = View.VISIBLE + lifecycleScope.launch { + val fresh = withContext(Dispatchers.IO) { + val session = app.mibSessions[loginId] + if (session != null && enabled.isNotEmpty()) { + try { + val accounts = app.mibFlowFor(loginId).fetchAllProfiles(session, enabled, loginTag) + if (accounts.isNotEmpty()) { + return@withContext app.mibAccounts.filter { it.loginTag != loginTag || it.profileId !in profileIds } + accounts + } + } catch (_: Exception) { } + } + // Session expired — log this one login in again (it now includes the unhidden profiles) + val store = CredentialStore(this@HomeActivity) + val creds = store.loadMibCredentials(loginId) ?: return@withContext null + try { + val flow = MibLoginFlow(store) + val accounts = flow.login(creds.username, creds.passwordHash, creds.otpSeed) + app.mibSessions[loginId] = flow.lastSession!! + app.mibProfilesMap[loginId] = flow.lastProfiles + app.mibLoginFlows[loginId] = flow + store.saveMibProfiles(loginId, flow.lastProfiles) + app.mibAccounts.filter { it.loginTag != loginTag } + accounts + } catch (_: Exception) { null } + } + binding.refreshIndicator.visibility = View.GONE + if (fresh == null) return@launch + app.mibAccounts = fresh + AccountCache.save(this@HomeActivity, fresh) + applyVisibility() + val session = app.mibSessions[loginId] ?: return@launch + val profiles = app.mibProfilesMap[loginId] ?: emptyList() + refreshMibCards(loginId, session, profiles.filter { it.profileId in profileIds }) + // Financing deals carry no profile id, so the login's visible profiles are fetched together + refreshFinancing(loginId, session, profiles.filterVisibleProfiles(loginId)) + } + } + + /** Fetches accounts (and loan details / limits) for BML profiles the user just unhid. */ + fun fetchEnabledBmlProfiles(loginId: String, profileIds: Set) { + if (profileIds.isEmpty()) return + val app = application as BasedBankApp + val store = CredentialStore(this) + val enabled = store.loadBmlProfiles(loginId).filter { it.profileId in profileIds } + if (enabled.isEmpty()) return + binding.refreshIndicator.visibility = View.VISIBLE + lifecycleScope.launch { + val fetched = withContext(Dispatchers.IO) { + enabled.mapNotNull { profile -> + try { fetchBmlProfileAccounts(store, loginId, profile)?.let { profile.profileId to it } } catch (_: Exception) { null } + } + } + binding.refreshIndicator.visibility = View.GONE + if (fetched.isEmpty()) return@launch + val fetchedIds = fetched.map { it.first }.toSet() + val loginTag = "bml_$loginId" + app.bmlAccounts = app.bmlAccounts.filter { it.loginTag != loginTag || it.profileId !in fetchedIds } + + fetched.flatMap { it.second } + AccountCache.saveBml(this@HomeActivity, loginId, app.bmlAccounts.filter { it.loginTag == loginTag }) + applyVisibility() + fetchedIds.forEach { id -> app.bmlSessions[id]?.let { refreshBmlLimits(it) } } + refreshBmlLoanDetails() + } + } + + /** + * Fetches one BML profile's accounts with its saved session, refreshing the token when expired. + * Returns null when the profile has no saved session; throws on network/server errors. + */ + private fun fetchBmlProfileAccounts(store: CredentialStore, loginId: String, profile: BmlProfile): List? { + val app = application as BasedBankApp + val saved = store.loadBmlProfileSession(profile.profileId) ?: return null + val refreshToken = store.loadBmlProfileRefreshToken(profile.profileId) + val expiresAt = store.loadBmlProfileExpiresAt(profile.profileId) + val tokenKnownExpired = expiresAt > 0L && System.currentTimeMillis() >= expiresAt + val bmlClient = BmlAccountClient() + + fun fetchWithSession(session: BmlSession): List { + bmlClient.checkProfile(session) + val accounts = bmlClient.fetchAccounts(session, "bml_$loginId", profile.name, profile.profileId) + app.bmlSessions[profile.profileId] = session + return accounts + } + + fun tryRefresh(): List { + if (refreshToken == null) throw Exception("No refresh token") + val oldSession = BmlSession(saved.first, saved.second, refreshToken) + val newSession = app.bmlFlowFor(loginId).refreshSession(oldSession) + store.saveBmlProfileSession(profile.profileId, newSession.accessToken, newSession.deviceId) + if (newSession.refreshToken.isNotBlank()) + store.saveBmlProfileRefreshToken(profile.profileId, newSession.refreshToken) + if (newSession.expiresAt > 0) + store.saveBmlProfileExpiresAt(profile.profileId, newSession.expiresAt) + return fetchWithSession(newSession) + } + + return if (tokenKnownExpired) { + tryRefresh() + } else { + try { + fetchWithSession(BmlSession(saved.first, saved.second)) + } catch (_: AuthExpiredException) { + tryRefresh() + } + } } private fun refreshBmlLimits(session: BmlSession) { @@ -1261,7 +1357,7 @@ fun applyNavLabelVisibility() { val app = application as BasedBankApp for ((loginId, session) in app.mibSessions) { val profiles = app.mibProfilesMap[loginId] ?: emptyList() - refreshMibCards(loginId, session, profiles) + refreshMibCards(loginId, session, profiles.filterVisibleProfiles(loginId)) } } @@ -1286,7 +1382,8 @@ fun applyNavLabelVisibility() { } if (cards.isNotEmpty()) { val existing = viewModel.mibCards.value?.toMutableList() ?: mutableListOf() - existing.removeAll { it.loginTag == "mib_$loginId" } + val fetchedIds = profiles.map { it.profileId }.toSet() + existing.removeAll { it.loginTag == "mib_$loginId" && (it.profileId in fetchedIds || it.profileId.isEmpty()) } existing += cards viewModel.mibCards.postValue(existing) CardsCache.save(this@HomeActivity, existing) 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 index f4a9969..4eefdc5 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/SettingsLoginsFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/SettingsLoginsFragment.kt @@ -573,6 +573,27 @@ class SettingsLoginsFragment : Fragment() { return accounts.map { addAccountRow(ctx, container, dp, it, hiddenAccounts, indent = false) } } + /** + * Unchecks and disables account toggles whose parent profile is hidden, and restores the + * account's own choice once the profile is shown again. [hiddenAccounts] is left untouched — + * the account listeners ignore changes made while a toggle is disabled. + */ + private fun syncAccountToggles( + accountRows: List>, + hiddenAccounts: Set, + isParentHidden: (BankAccount) -> Boolean + ) { + accountRows.forEach { (acc, toggle) -> + if (isParentHidden(acc)) { + toggle.isEnabled = false + toggle.isChecked = false + } else { + toggle.isChecked = acc.accountNumber !in hiddenAccounts + toggle.isEnabled = true + } + } + } + /** Merges this login's account-hide choices into the global hidden-accounts set, leaving other logins untouched. */ private fun persistHiddenAccounts(store: CredentialStore, scopedAccounts: List, hiddenAccounts: Set) { val scopedNumbers = scopedAccounts.map { it.accountNumber }.toSet() @@ -696,6 +717,7 @@ class SettingsLoginsFragment : Fragment() { // Disable the sole remaining visible toggle so it can't be turned off toggle.isEnabled = !(toggle.isChecked && visibleCount == 1) } + syncAccountToggles(accountRows, hiddenAccounts) { it.profileId in hidden } saveBtn.isEnabled = (hidden != originalHidden || hiddenAccounts != originalHiddenAccounts) && visibleCount >= 1 } @@ -722,6 +744,7 @@ class SettingsLoginsFragment : Fragment() { accountRows.forEach { (acc, toggle) -> toggle.setOnCheckedChangeListener { _, checked -> + if (!toggle.isEnabled) return@setOnCheckedChangeListener // driven by a hidden parent profile if (checked) hiddenAccounts.remove(acc.accountNumber) else hiddenAccounts.add(acc.accountNumber) updateToggleStates(saveBtn) } @@ -730,9 +753,11 @@ class SettingsLoginsFragment : Fragment() { saveBtn.setOnClickListener { store.setHiddenMibProfileIds(loginId, hidden) persistHiddenAccounts(store, loginAccounts, hiddenAccounts) - clearAllCaches(ctx) dialog.dismiss() - (activity as? HomeActivity)?.relogin() + // Hiding is applied offline; only profiles that were just unhidden get fetched + val home = activity as? HomeActivity + home?.applyVisibility() + home?.fetchEnabledMibProfiles(loginId, originalHidden - hidden) } } @@ -869,6 +894,7 @@ class SettingsLoginsFragment : Fragment() { toggleRows.forEach { (_, toggle) -> toggle.isEnabled = !(toggle.isChecked && visibleCount == 1) } + syncAccountToggles(accountRows, hiddenAccounts) { it.profileId in hidden } saveBtn.isEnabled = (hidden != originalHidden || hiddenAccounts != originalHiddenAccounts) && visibleCount >= 1 } @@ -906,6 +932,7 @@ class SettingsLoginsFragment : Fragment() { accountRows.forEach { (acc, toggle) -> toggle.setOnCheckedChangeListener { _, checked -> + if (!toggle.isEnabled) return@setOnCheckedChangeListener // driven by a hidden parent profile if (checked) hiddenAccounts.remove(acc.accountNumber) else hiddenAccounts.add(acc.accountNumber) updateToggleStates(saveBtn) } @@ -914,9 +941,11 @@ class SettingsLoginsFragment : Fragment() { saveBtn.setOnClickListener { store.setHiddenBmlProfileIds(loginId, hidden) persistHiddenAccounts(store, loginAccounts, hiddenAccounts) - clearAllCaches(ctx) dialog.dismiss() - (activity as? HomeActivity)?.relogin() + // Hiding is applied offline; only profiles that were just unhidden get fetched + val home = activity as? HomeActivity + home?.applyVisibility() + home?.fetchEnabledBmlProfiles(loginId, originalHidden - hidden) } } @@ -1220,9 +1249,8 @@ class SettingsLoginsFragment : Fragment() { saveBtn.setOnClickListener { persistHiddenAccounts(store, loginAccounts, hiddenAccounts) - clearAllCaches(ctx) dialog.dismiss() - (activity as? HomeActivity)?.relogin() + (activity as? HomeActivity)?.applyVisibility() } } } @@ -1398,6 +1426,7 @@ class SettingsLoginsFragment : Fragment() { toggleRows.forEach { (_, toggle) -> toggle.isEnabled = !(toggle.isChecked && visibleCount == 1) } + syncAccountToggles(accountRows, hiddenAccounts) { it.accountNumber in hidden } saveBtn.isEnabled = (hidden != originalHidden || hiddenAccounts != originalHiddenAccounts) && visibleCount >= 1 } @@ -1427,17 +1456,18 @@ class SettingsLoginsFragment : Fragment() { accountRows.forEach { (acc, toggle) -> toggle.setOnCheckedChangeListener { _, checked -> + if (!toggle.isEnabled) return@setOnCheckedChangeListener // driven by a hidden pocket group if (checked) hiddenAccounts.remove(acc.accountNumber) else hiddenAccounts.add(acc.accountNumber) updateToggleStates(saveBtn) } } saveBtn.setOnClickListener { + // All pockets come back in one login response, so hiding/unhiding never needs a request store.setHiddenMfaisaPocketIds(loginId, hidden) persistHiddenAccounts(store, pockets, hiddenAccounts) - clearAllCaches(ctx) dialog.dismiss() - (activity as? HomeActivity)?.relogin() + (activity as? HomeActivity)?.applyVisibility() } } }