Establishing a scope for each account loaded.

This commit is contained in:
Vitor Pamplona
2025-09-08 14:34:13 -04:00
parent 93a639fea5
commit 222615a116
5 changed files with 57 additions and 41 deletions

View File

@@ -26,8 +26,6 @@ import android.util.Log
import androidx.security.crypto.EncryptedSharedPreferences
import coil3.disk.DiskCache
import coil3.memory.MemoryCache
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.accountsCache.AccountCacheState
import com.vitorpamplona.amethyst.service.connectivity.ConnectivityManager
@@ -54,13 +52,9 @@ import com.vitorpamplona.amethyst.service.relayClient.speedLogger.RelaySpeedLogg
import com.vitorpamplona.amethyst.service.uploads.nip95.Nip95CacheFactory
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav.scope
import com.vitorpamplona.amethyst.ui.tor.TorManager
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip03Timestamp.VerificationStateCache
import com.vitorpamplona.quartz.nip03Timestamp.ots.okhttp.OtsBlockHeightCache
import com.vitorpamplona.quartz.nip55AndroidSigner.client.NostrSignerExternal
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -161,13 +155,14 @@ class Amethyst : Application() {
// Coordinates all subscriptions for the Nostr Client
val sources: RelaySubscriptionsCoordinator = RelaySubscriptionsCoordinator(LocalCache, client, applicationIOScope)
// keeps all accounts live
val accountsCache =
AccountCacheState(
geolocationFlow = locationManager.geohashStateFlow,
nwcFilterAssembler = sources.nwc,
contentResolver = contentResolver,
cache = cache,
client = client,
scope = applicationIOScope,
)
// saves the .content of NIP-95 blobs in disk to save memory
@@ -242,31 +237,6 @@ class Amethyst : Application() {
}
}
fun loadAccount(accountSettings: AccountSettings): Account {
val keyPair = accountSettings.keyPair
return accountsCache.loadAccount(
signer =
if (keyPair.privKey != null) {
NostrSignerInternal(keyPair)
} else {
when (val packageName = accountSettings.externalSignerPackageName) {
null -> NostrSignerInternal(keyPair)
else ->
NostrSignerExternal(
pubKey = keyPair.pubKey.toHexKey(),
packageName = packageName,
contentResolver = contentResolver,
)
}
},
accountSettings = accountSettings,
)
}
fun removeAccount(pubkey: HexKey) {
accountsCache.removeAccount(pubkey)
}
companion object {
lateinit var instance: Amethyst
private set

View File

@@ -20,6 +20,8 @@
*/
package com.vitorpamplona.amethyst.model.accountsCache
import android.content.ContentResolver
import android.util.Log
import androidx.collection.LruCache
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.AccountSettings
@@ -27,22 +29,59 @@ import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.location.LocationState
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip55AndroidSigner.client.NostrSignerExternal
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.StateFlow
class AccountCacheState(
val geolocationFlow: StateFlow<LocationState.LocationResult>,
val nwcFilterAssembler: NWCPaymentFilterAssembler,
val contentResolver: ContentResolver,
val cache: LocalCache,
val client: INostrClient,
val scope: CoroutineScope,
) {
val accounts = LruCache<HexKey, Account>(20)
val accounts =
object : LruCache<HexKey, Account>(20) {
override fun entryRemoved(
evicted: Boolean,
key: HexKey,
oldValue: Account,
newValue: Account?,
) {
super.entryRemoved(evicted, key, oldValue, newValue)
oldValue.scope.cancel()
}
}
fun removeAccount(pubkey: HexKey) = accounts.remove(pubkey)
fun loadAccount(accountSettings: AccountSettings): Account =
loadAccount(
signer =
if (accountSettings.keyPair.privKey != null) {
NostrSignerInternal(accountSettings.keyPair)
} else {
when (val packageName = accountSettings.externalSignerPackageName) {
null -> NostrSignerInternal(accountSettings.keyPair)
else ->
NostrSignerExternal(
pubKey = accountSettings.keyPair.pubKey.toHexKey(),
packageName = packageName,
contentResolver = contentResolver,
)
}
},
accountSettings = accountSettings,
)
fun loadAccount(
signer: NostrSigner,
accountSettings: AccountSettings,
@@ -57,7 +96,14 @@ class AccountCacheState(
nwcFilterAssembler = nwcFilterAssembler,
cache = cache,
client = client,
scope = scope,
scope =
CoroutineScope(
Dispatchers.Default +
SupervisorJob() +
CoroutineExceptionHandler { _, throwable ->
Log.e("AccountCacheState", "Account ${signer.pubKey} caught exception: ${throwable.message}", throwable)
},
),
).also {
accounts.put(signer.pubKey, it)
}

View File

@@ -71,7 +71,7 @@ class EventNotificationConsumer(
LocalPreferences.loadAccountConfigFromEncryptedStorage(it.npub)?.let { acc ->
Log.d(TAG, "New Notification Testing if for ${it.npub}")
try {
val account = Amethyst.instance.loadAccount(acc)
val account = Amethyst.instance.accountsCache.loadAccount(acc)
consumeIfMatchesAccount(event, account)
matchAccount = true
} catch (e: Exception) {
@@ -128,7 +128,7 @@ class EventNotificationConsumer(
LocalPreferences.loadAccountConfigFromEncryptedStorage(it.npub)?.let { accountSettings ->
Log.d(TAG, "New Notification Testing if for ${it.npub}")
try {
val account = Amethyst.instance.loadAccount(accountSettings)
val account = Amethyst.instance.accountsCache.loadAccount(accountSettings)
consumeNotificationEvent(event, account)
matchAccount = true
} catch (e: Exception) {

View File

@@ -59,7 +59,7 @@ class RegisterAccounts(
}
return mapNotNullAsync(remainingTos) { info ->
val account = Amethyst.instance.loadAccount(info.accountSettings)
val account = Amethyst.instance.accountsCache.loadAccount(info.accountSettings)
RelayAuthEvent.create(info.relays, notificationToken, account.signer)
}
}

View File

@@ -185,7 +185,7 @@ class AccountStateViewModel : ViewModel() {
route: Route? = null,
) = withContext(Dispatchers.Main) {
_accountContent.update {
AccountState.LoggedIn(Amethyst.instance.loadAccount(accountSettings), route)
AccountState.LoggedIn(Amethyst.instance.accountsCache.loadAccount(accountSettings), route)
}
collectorJob?.cancel()
@@ -390,12 +390,12 @@ class AccountStateViewModel : ViewModel() {
// log off and relogin with the 0 account
prepareLogoutOrSwitch()
LocalPreferences.deleteAccount(accountInfo)
Amethyst.instance.removeAccount(accountInfo.npub.bechToBytes().toHexKey())
Amethyst.instance.accountsCache.removeAccount(accountInfo.npub.bechToBytes().toHexKey())
loginWithDefaultAccount()
} else {
// delete without switching logins
LocalPreferences.deleteAccount(accountInfo)
Amethyst.instance.removeAccount(accountInfo.npub.bechToBytes().toHexKey())
Amethyst.instance.accountsCache.removeAccount(accountInfo.npub.bechToBytes().toHexKey())
}
}
}