package sh.sar.basedbank import android.app.Application import androidx.appcompat.app.AppCompatDelegate import com.google.android.material.color.DynamicColors import kotlinx.coroutines.sync.Mutex import sh.sar.basedbank.api.bml.BmlLoginFlow import sh.sar.basedbank.api.bml.BmlProfile import sh.sar.basedbank.api.bml.BmlSession import sh.sar.basedbank.api.fahipay.FahipaySession import sh.sar.basedbank.api.mfaisa.MfaisaSession import sh.sar.basedbank.api.models.BankAccount import sh.sar.basedbank.api.mib.MibLoginFlow import sh.sar.basedbank.api.mib.MibProfile import sh.sar.basedbank.api.mib.MibSession import sh.sar.basedbank.util.CredentialStore class BasedBankApp : Application() { /** * Set to true only after the user passes LockActivity or completes fresh login. * Resets to false on every process restart so direct ADB/root activity launches * cannot reach HomeActivity without re-authenticating. */ var isUnlocked = false // Held in memory after successful login; cleared on logout var accounts: List = emptyList() var fullName: String = "" /** Active MIB sessions keyed by loginId (= MIB username). */ val mibSessions: MutableMap = mutableMapOf() val mibProfilesMap: MutableMap> = mutableMapOf() val mibLoginFlows: MutableMap = mutableMapOf() var mibAccounts: List = emptyList() /** * Active BML sessions keyed by profileId (a globally unique GUID per BML profile). * Use [bmlSessionFor] to look up the session for an account. */ val bmlSessions: MutableMap = mutableMapOf() /** BML profiles per loginId (= BML username). */ val bmlProfilesMap: MutableMap> = mutableMapOf() /** BML login flows per loginId — hold the web session (cookies) needed for profile activation. */ val bmlLoginFlows: MutableMap = mutableMapOf() var bmlAccounts: List = emptyList() /** Active Fahipay sessions keyed by loginId (= profileId). */ val fahipaySessions: MutableMap = mutableMapOf() var fahipayAccounts: List = emptyList() /** Active M-Faisa sessions keyed by loginId (= msisdn). */ val mfaisaSessions: MutableMap = mutableMapOf() var mfaisaAccounts: List = emptyList() // ─── MIB helpers ────────────────────────────────────────────────────────── /** Returns the MIB session for the given account (matched via loginTag). */ fun mibSessionFor(account: BankAccount): MibSession? = mibSessions[account.loginTag.removePrefix("mib_")] /** Returns any available MIB session. */ fun anyMibSession(): MibSession? = mibSessions.values.firstOrNull() /** Returns all MIB profiles across all logins. */ fun allMibProfiles(): List = mibProfilesMap.values.flatten() /** Returns the MibLoginFlow for a given loginId, creating and caching it if needed. */ fun mibFlowFor(loginId: String): MibLoginFlow = mibLoginFlows.getOrPut(loginId) { MibLoginFlow(CredentialStore(this)).also { flow -> flow.onSessionRefreshed = { session, profiles -> mibSessions[loginId] = session mibProfilesMap[loginId] = profiles } } } /** Returns any available MibLoginFlow. */ fun anyMibFlow(): MibLoginFlow? = mibLoginFlows.values.firstOrNull() // ─── BML helpers ────────────────────────────────────────────────────────── /** * Returns the BML session for the given account. * Looks up by profileId first (multi-profile), falls back to loginId (legacy single-profile). */ fun bmlSessionFor(account: BankAccount): BmlSession? { val byProfile = if (account.profileId.isNotBlank()) bmlSessions[account.profileId] else null return byProfile ?: bmlSessions[account.loginTag.removePrefix("bml_")] } /** Returns any available BML session (for non-account-specific operations). */ fun anyBmlSession(): BmlSession? = bmlSessions.values.firstOrNull() /** * Returns any active BML session for the given loginId. * Tries all profiles for that login; falls back to legacy loginId key. */ fun anyBmlSessionFor(loginId: String): BmlSession? { val profiles = bmlProfilesMap[loginId] if (!profiles.isNullOrEmpty()) { return profiles.firstNotNullOfOrNull { bmlSessions[it.profileId] } } return bmlSessions[loginId] } /** Returns the BmlLoginFlow for a given loginId, creating and caching it if needed. */ fun bmlFlowFor(loginId: String): BmlLoginFlow = bmlLoginFlows.getOrPut(loginId) { BmlLoginFlow() } // ─── Fahipay helpers ────────────────────────────────────────────────────── /** Returns the Fahipay session for the given account (matched via loginTag = "fahipay_${profileId}"). */ fun fahipaySessionFor(account: BankAccount): FahipaySession? = fahipaySessions[account.loginTag.removePrefix("fahipay_")] // ─── M-Faisa helpers ────────────────────────────────────────────────────── /** Returns the M-Faisa session for the given account (matched via loginTag = "mfaisa_${msisdn}"). */ fun mfaisaSessionFor(account: BankAccount): MfaisaSession? = mfaisaSessions[account.loginTag.removePrefix("mfaisa_")] /** * Re-runs `fetchSubscriber` + `doMobileLogin` using the saved credentials for [loginId] and * replaces the cached session. Call this after catching [sh.sar.basedbank.api.mfaisa.MfaisaSessionExpiredException]. * Returns the fresh session, or null if no credentials are saved for that login. */ fun refreshMfaisaSession(loginId: String): MfaisaSession? { val creds = CredentialStore(this).loadMfaisaCredentials(loginId) ?: return null val flow = sh.sar.basedbank.api.mfaisa.MfaisaLoginFlow(this) flow.fetchSubscriber(creds.msisdn) val result = flow.doMobileLogin(creds.msisdn, creds.pin) mfaisaSessions[loginId] = result.session return result.session } /** Serialises all MIB profile-switch + request sequences to prevent session corruption. */ val mibMutex = Mutex() override fun onCreate() { super.onCreate() // Only apply wallpaper-based dynamic colors in system theme mode. // Light/dark modes use content-based accent colors applied per-activity via ThemeHelper. DynamicColors.applyToActivitiesIfAvailable(this) { _, _ -> getSharedPreferences("prefs", MODE_PRIVATE).getString("theme", "system") == "system" } val theme = getSharedPreferences("prefs", MODE_PRIVATE).getString("theme", "system") AppCompatDelegate.setDefaultNightMode(when (theme) { "dark" -> AppCompatDelegate.MODE_NIGHT_YES "light" -> AppCompatDelegate.MODE_NIGHT_NO else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM }) } }