lazy loading

This commit is contained in:
2026-05-14 05:24:14 +05:00
parent 81a2be150f
commit 7026da9ccd
11 changed files with 131 additions and 75 deletions
@@ -11,7 +11,7 @@ import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import sh.sar.basedbank.databinding.ActivityLockBinding import sh.sar.basedbank.databinding.ActivityLockBinding
import sh.sar.basedbank.ui.login.LoginActivity import sh.sar.basedbank.ui.home.HomeActivity
import java.security.MessageDigest import java.security.MessageDigest
class LockActivity : AppCompatActivity() { class LockActivity : AppCompatActivity() {
@@ -154,7 +154,7 @@ class LockActivity : AppCompatActivity() {
} }
private fun proceed() { private fun proceed() {
startActivity(Intent(this, LoginActivity::class.java)) startActivity(Intent(this, HomeActivity::class.java))
finish() finish()
} }
@@ -3,8 +3,10 @@ package sh.sar.basedbank
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import sh.sar.basedbank.ui.home.HomeActivity
import sh.sar.basedbank.ui.login.LoginActivity import sh.sar.basedbank.ui.login.LoginActivity
import sh.sar.basedbank.ui.onboarding.OnboardingActivity import sh.sar.basedbank.ui.onboarding.OnboardingActivity
import sh.sar.basedbank.util.CredentialStore
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
@@ -13,10 +15,13 @@ class MainActivity : AppCompatActivity() {
val prefs = getSharedPreferences("prefs", MODE_PRIVATE) val prefs = getSharedPreferences("prefs", MODE_PRIVATE)
val onboardingDone = prefs.getBoolean("onboarding_done", false) val onboardingDone = prefs.getBoolean("onboarding_done", false)
val securitySet = prefs.getString("security_method", null) != null val securitySet = prefs.getString("security_method", null) != null
val hasCredentials = CredentialStore(this).hasMibCredentials()
val target = when { val target = when {
!onboardingDone -> OnboardingActivity::class.java !onboardingDone -> OnboardingActivity::class.java
securitySet -> LockActivity::class.java !hasCredentials -> LoginActivity::class.java
else -> LoginActivity::class.java securitySet -> LockActivity::class.java // proceed() → HomeActivity
else -> HomeActivity::class.java
} }
startActivity(Intent(this, target)) startActivity(Intent(this, target))
finish() finish()
@@ -7,7 +7,7 @@ import sh.sar.basedbank.api.mib.MibAccount
import sh.sar.basedbank.databinding.ItemAccountBinding import sh.sar.basedbank.databinding.ItemAccountBinding
import sh.sar.basedbank.databinding.ItemProfileHeaderBinding import sh.sar.basedbank.databinding.ItemProfileHeaderBinding
class AccountsAdapter(private val accounts: List<MibAccount>) : class AccountsAdapter(accounts: List<MibAccount>) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() { RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private sealed class Item { private sealed class Item {
@@ -15,7 +15,15 @@ class AccountsAdapter(private val accounts: List<MibAccount>) :
data class Account(val account: MibAccount) : Item() data class Account(val account: MibAccount) : Item()
} }
private val items: List<Item> = buildList { private val items: MutableList<Item> = buildItems(accounts).toMutableList()
fun updateAccounts(accounts: List<MibAccount>) {
items.clear()
items.addAll(buildItems(accounts))
notifyDataSetChanged()
}
private fun buildItems(accounts: List<MibAccount>): List<Item> = buildList {
var lastProfile = "" var lastProfile = ""
for (account in accounts) { for (account in accounts) {
if (account.profileName != lastProfile) { if (account.profileName != lastProfile) {
@@ -1,26 +1,66 @@
package sh.sar.basedbank.ui.home package sh.sar.basedbank.ui.home
import android.os.Bundle import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import sh.sar.basedbank.BasedBankApp import sh.sar.basedbank.BasedBankApp
import sh.sar.basedbank.api.mib.MibLoginFlow
import sh.sar.basedbank.databinding.ActivityHomeBinding import sh.sar.basedbank.databinding.ActivityHomeBinding
import sh.sar.basedbank.util.AccountCache
import sh.sar.basedbank.util.CredentialStore
class HomeActivity : AppCompatActivity() { class HomeActivity : AppCompatActivity() {
private lateinit var binding: ActivityHomeBinding private lateinit var binding: ActivityHomeBinding
private lateinit var adapter: AccountsAdapter
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityHomeBinding.inflate(layoutInflater) binding = ActivityHomeBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
setSupportActionBar(binding.toolbar) setSupportActionBar(binding.toolbar)
val accounts = (application as BasedBankApp).accounts val app = application as BasedBankApp
val adapter = AccountsAdapter(accounts)
// If we arrived here from a fresh manual login, accounts are already in memory.
// Otherwise load the last-known cache so the UI is instant.
val initial = if (app.accounts.isNotEmpty()) app.accounts else AccountCache.load(this)
adapter = AccountsAdapter(initial)
binding.recyclerView.layoutManager = LinearLayoutManager(this) binding.recyclerView.layoutManager = LinearLayoutManager(this)
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
if (app.accounts.isNotEmpty()) {
// Just logged in — persist the fresh data and we're done
AccountCache.save(this, app.accounts)
} else {
// Came from lock screen — refresh in background
val creds = CredentialStore(this).loadMibCredentials()
if (creds != null) autoRefresh(creds)
}
}
private fun autoRefresh(creds: CredentialStore.MibCredentials) {
binding.refreshIndicator.visibility = View.VISIBLE
val prefs = getSharedPreferences("mib_prefs", MODE_PRIVATE)
val flow = MibLoginFlow(prefs)
lifecycleScope.launch {
try {
val accounts = withContext(Dispatchers.IO) {
flow.login(creds.username, creds.passwordHash, creds.otpSeed)
}
(application as BasedBankApp).accounts = accounts
AccountCache.save(this@HomeActivity, accounts)
adapter.updateAccounts(accounts)
} catch (_: Exception) {
// Keep showing cached data silently
} finally {
binding.refreshIndicator.visibility = View.GONE
}
}
} }
} }
@@ -18,6 +18,7 @@ import kotlinx.coroutines.withContext
import sh.sar.basedbank.util.Totp import sh.sar.basedbank.util.Totp
import sh.sar.basedbank.BasedBankApp import sh.sar.basedbank.BasedBankApp
import sh.sar.basedbank.api.mib.MibLoginFlow import sh.sar.basedbank.api.mib.MibLoginFlow
import sh.sar.basedbank.util.AccountCache
import sh.sar.basedbank.util.CredentialStore import sh.sar.basedbank.util.CredentialStore
import sh.sar.basedbank.databinding.FragmentCredentialsBinding import sh.sar.basedbank.databinding.FragmentCredentialsBinding
import sh.sar.basedbank.ui.home.HomeActivity import sh.sar.basedbank.ui.home.HomeActivity
@@ -111,6 +112,7 @@ class CredentialsFragment : Fragment() {
} }
Log.d(TAG, "Login succeeded, got ${accounts.size} accounts") Log.d(TAG, "Login succeeded, got ${accounts.size} accounts")
CredentialStore(requireContext()).saveMibCredentials(username, passwordHash, otpSeed) CredentialStore(requireContext()).saveMibCredentials(username, passwordHash, otpSeed)
AccountCache.save(requireContext(), accounts)
(requireActivity().application as BasedBankApp).accounts = accounts (requireActivity().application as BasedBankApp).accounts = accounts
startActivity(Intent(requireContext(), HomeActivity::class.java)) startActivity(Intent(requireContext(), HomeActivity::class.java))
requireActivity().finish() requireActivity().finish()
@@ -1,19 +1,8 @@
package sh.sar.basedbank.ui.login package sh.sar.basedbank.ui.login
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import sh.sar.basedbank.BasedBankApp
import sh.sar.basedbank.R
import sh.sar.basedbank.api.mib.MibLoginFlow
import sh.sar.basedbank.databinding.ActivityLoginBinding import sh.sar.basedbank.databinding.ActivityLoginBinding
import sh.sar.basedbank.ui.home.HomeActivity
import sh.sar.basedbank.util.CredentialStore
class LoginActivity : AppCompatActivity() { class LoginActivity : AppCompatActivity() {
@@ -23,31 +12,5 @@ class LoginActivity : AppCompatActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityLoginBinding.inflate(layoutInflater) binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
val creds = CredentialStore(this).loadMibCredentials()
if (creds != null) {
binding.navHostFragment.visibility = View.GONE
binding.autoLoginGroup.visibility = View.VISIBLE
autoLogin(creds)
}
}
private fun autoLogin(creds: CredentialStore.MibCredentials) {
val prefs = getSharedPreferences("mib_prefs", MODE_PRIVATE)
val flow = MibLoginFlow(prefs)
lifecycleScope.launch {
try {
val accounts = withContext(Dispatchers.IO) {
flow.login(creds.username, creds.passwordHash, creds.otpSeed)
}
(application as BasedBankApp).accounts = accounts
startActivity(Intent(this@LoginActivity, HomeActivity::class.java))
finish()
} catch (e: Exception) {
// Auto-login failed — fall back to manual login form
binding.autoLoginGroup.visibility = View.GONE
binding.navHostFragment.visibility = View.VISIBLE
}
}
} }
} }
@@ -0,0 +1,59 @@
package sh.sar.basedbank.util
import android.content.Context
import org.json.JSONArray
import org.json.JSONObject
import sh.sar.basedbank.api.mib.MibAccount
object AccountCache {
private const val PREFS = "account_cache"
private const val KEY_MIB = "mib_accounts"
fun save(context: Context, accounts: List<MibAccount>) {
val arr = JSONArray()
for (acc in accounts) {
arr.put(JSONObject().apply {
put("profileName", acc.profileName)
put("profileType", acc.profileType)
put("accountNumber", acc.accountNumber)
put("accountBriefName", acc.accountBriefName)
put("currencyName", acc.currencyName)
put("accountTypeName", acc.accountTypeName)
put("availableBalance", acc.availableBalance)
put("currentBalance", acc.currentBalance)
put("blockedAmount", acc.blockedAmount)
put("mvrBalance", acc.mvrBalance)
put("statusDesc", acc.statusDesc)
})
}
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
.edit().putString(KEY_MIB, arr.toString()).apply()
}
fun load(context: Context): List<MibAccount> {
val json = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
.getString(KEY_MIB, null) ?: return emptyList()
return try {
val arr = JSONArray(json)
(0 until arr.length()).map { i ->
val o = arr.getJSONObject(i)
MibAccount(
profileName = o.optString("profileName"),
profileType = o.optString("profileType"),
accountNumber = o.optString("accountNumber"),
accountBriefName = o.optString("accountBriefName"),
currencyName = o.optString("currencyName"),
accountTypeName = o.optString("accountTypeName"),
availableBalance = o.optString("availableBalance"),
currentBalance = o.optString("currentBalance"),
blockedAmount = o.optString("blockedAmount"),
mvrBalance = o.optString("mvrBalance"),
statusDesc = o.optString("statusDesc")
)
}
} catch (e: Exception) {
emptyList()
}
}
}
@@ -19,6 +19,14 @@
app:title="@string/accounts" app:title="@string/accounts"
app:titleTextAppearance="?attr/textAppearanceTitleLarge" /> app:titleTextAppearance="?attr/textAppearanceTitleLarge" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/refreshIndicator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:indeterminate="true"
app:trackCornerRadius="0dp" />
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
@@ -18,29 +18,4 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" /> app:layout_constraintEnd_toEndOf="parent" />
<LinearLayout
android:id="@+id/autoLoginGroup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:visibility="gone"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<ProgressBar
android:layout_width="48dp"
android:layout_height="48dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/signing_in"
android:textAppearance="?attr/textAppearanceBodyMedium" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
-2
View File
@@ -60,8 +60,6 @@
<string name="skip_biometrics">ސްކިޕް — PIN/ޕެޓަން ބޭނުން ކުރޭ</string> <string name="skip_biometrics">ސްކިޕް — PIN/ޕެޓަން ބޭނުން ކުރޭ</string>
<string name="back">ފަހަތަށް</string> <string name="back">ފަހަތަށް</string>
<string name="signing_in">ލޮގިން ވަނީ…</string>
<!-- Home --> <!-- Home -->
<string name="accounts">އެކައުންޓްތައް</string> <string name="accounts">އެކައުންޓްތައް</string>
<string name="available_balance">ލިބެން ހުރި ބެލެންސް</string> <string name="available_balance">ލިބެން ހުރި ބެލެންސް</string>
-2
View File
@@ -59,8 +59,6 @@
<string name="skip_biometrics">Skip — use PIN/Pattern only</string> <string name="skip_biometrics">Skip — use PIN/Pattern only</string>
<string name="back">Back</string> <string name="back">Back</string>
<string name="signing_in">Signing in…</string>
<!-- Home --> <!-- Home -->
<string name="accounts">Accounts</string> <string name="accounts">Accounts</string>
<string name="available_balance">Available Balance</string> <string name="available_balance">Available Balance</string>