commit 51be6a213ec062dbc5576c31103f5e93ef67bcad
parent 9eb549294279c8eebded56cc94892dd364c110eb
Author: t3sserakt <t3ss@posteo.de>
Date: Wed, 13 May 2026 14:06:33 +0200
moved to single handled application
Diffstat:
2 files changed, 48 insertions(+), 208 deletions(-)
diff --git a/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/MainActivity.kt b/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/MainActivity.kt
@@ -46,7 +46,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
-import kotlinx.coroutines.withTimeout
import java.util.concurrent.ConcurrentHashMap
import org.gnunet.gnunetmessenger.model.ChatAccount
import org.gnunet.gnunetmessenger.model.ChatContact
@@ -64,19 +63,6 @@ import org.gnunet.gnunetmessenger.viewmodel.ChatOverviewViewModel
import org.gnunet.gnunetmessenger.viewmodel.ChatViewModel
import org.gnunet.gnunetmessenger.viewmodel.ContactListViewModel
-/**
- * One connected account's full state: its dedicated bound-service instance,
- * its chat handle on the daemon, and the user-facing account itself. Multi-
- * handle architecture: each account the user activates gets its own session,
- * all kept connected for the lifetime of the process. Mirrors how
- * messenger-gtk runs two processes against one daemon.
- */
-data class AccountSession(
- val account: ChatAccount,
- val handle: ChatHandle,
- val gnunetChat: GnunetChat
-)
-
class MainActivity : AppCompatActivity() {
private lateinit var gnunetChat: GnunetChat
@@ -84,8 +70,6 @@ class MainActivity : AppCompatActivity() {
private lateinit var appBarConfiguration: AppBarConfiguration
private lateinit var handle: ChatHandle
- /** Session registry keyed by lower-cased account name. */
- private val sessions = mutableMapOf<String, AccountSession>()
private val chatReady = CompletableDeferred<ChatHandle>()
private val initialRefreshReady = CompletableDeferred<Unit>()
@@ -197,11 +181,6 @@ class MainActivity : AppCompatActivity() {
private fun processChatMessage(chatContext: ChatContext, chatMessage: ChatMessage) {
requireNotNull(chatMessage)
- // Shadow the singleton fields with the foreground session's instances
- // so the body of this handler always operates against the live account.
- val gnunetChat = getCurrentService()
- val handle = getCurrentHandle()
-
Log.d(
TAG,
"processChatMessage: kind=${chatMessage.kind} " +
@@ -300,6 +279,14 @@ class MainActivity : AppCompatActivity() {
)
}
viewModel?.addMessage(displayMessage)
+
+ val acctName = currentAccount?.name ?: ""
+ val chatKey = stableChatKey(chatContext)
+ if (acctName.isNotEmpty() && chatKey != null) {
+ viewModel?.messages?.value?.let { msgs ->
+ org.gnunet.gnunetmessenger.utils.MessageStorage.saveMessages(this, acctName, chatKey, msgs)
+ }
+ }
}
MessageKind.CONTACT,
@@ -333,6 +320,14 @@ class MainActivity : AppCompatActivity() {
if (localChatContext != null) {
viewModel?.addMessage(chatMessage)
chatOverviewViewModel.updateChatMessage(localChatContext, chatMessage)
+
+ val acctName = currentAccount?.name ?: ""
+ val chatKey = stableChatKey(chatContext)
+ if (acctName.isNotEmpty() && chatKey != null) {
+ viewModel?.messages?.value?.let { msgs ->
+ org.gnunet.gnunetmessenger.utils.MessageStorage.saveMessages(this, acctName, chatKey, msgs)
+ }
+ }
}
}
@@ -362,7 +357,6 @@ class MainActivity : AppCompatActivity() {
// and is blank for newly-built contexts). This is what we key the
// per-chat ViewModels by so that messages survive navigation.
private fun stableChatKey(chatContext: ChatContext): String? {
- val gnunetChat = getCurrentService()
runCatching { gnunetChat.getGroupFromContext(chatContext) }
.getOrNull()
?.takeIf { it.name.isNotBlank() }
@@ -381,6 +375,17 @@ class MainActivity : AppCompatActivity() {
val key = stableChatKey(chatContext) ?: return null
val isNew = key !in chatViewModels
val vm = chatViewModels.getOrPut(key) { ChatViewModel(chatContext) }
+
+ if (isNew) {
+ val accountName = currentAccount?.name ?: ""
+ if (accountName.isNotEmpty()) {
+ val msgs = org.gnunet.gnunetmessenger.utils.MessageStorage.loadMessages(this, accountName, key)
+ if (msgs.isNotEmpty()) {
+ vm.setMessages(msgs)
+ }
+ }
+ }
+
Log.d(TAG, "getChatViewModel key=$key isNew=$isNew msgCount=${vm.messages.value?.size ?: 0}")
return vm
}
@@ -392,6 +397,16 @@ class MainActivity : AppCompatActivity() {
}
}
+ fun saveChatMessages(chatContext: ChatContext, viewModel: ChatViewModel) {
+ val acctName = currentAccount?.name ?: ""
+ val chatKey = stableChatKey(chatContext)
+ if (acctName.isNotEmpty() && chatKey != null) {
+ viewModel.messages.value?.let { msgs ->
+ org.gnunet.gnunetmessenger.utils.MessageStorage.saveMessages(this, acctName, chatKey, msgs)
+ }
+ }
+ }
+
fun loadChats() {
lifecycleScope.launch {
try {
@@ -407,11 +422,6 @@ class MainActivity : AppCompatActivity() {
}
private suspend fun loadChatsSuspend() = loadChatsMutex.withLock {
- // Capture the foreground session's instances; without this we'd
- // iterate against the bootstrap handle (no connected account → SYSERR).
- val gnunetChat = getCurrentService()
- val handle = getCurrentHandle()
-
withContext(Dispatchers.IO) {
val summaries = mutableListOf<ChatSummary>()
val contacts = mutableListOf<ChatContact>()
@@ -607,185 +617,12 @@ class MainActivity : AppCompatActivity() {
super.onSupportNavigateUp()
}
- fun getGnunetChatInstance(): GnunetChat = getCurrentService()
+ fun getGnunetChatInstance(): GnunetChat = gnunetChat
- fun getChatHandle(): ChatHandle = getCurrentHandle()
-
- /** The session for the currently selected account, or null when none. */
- fun currentSession(): AccountSession? =
- currentAccount?.name?.lowercase()?.let { sessions[it] }
-
- /**
- * The bound-service instance the UI should use right now. Falls back to
- * the bootstrap singleton when no per-account session is registered.
- */
- fun getCurrentService(): GnunetChat = currentSession()?.gnunetChat ?: gnunetChat
-
- /**
- * The chat handle the UI should use right now. Falls back to the
- * bootstrap singleton handle when no per-account session is registered.
- */
- fun getCurrentHandle(): ChatHandle = currentSession()?.handle ?: handle
+ fun getChatHandle(): ChatHandle = handle
fun setCurrentAccount(account: ChatAccount) {
currentAccount = account
invalidateOptionsMenu()
}
-
- /**
- * Spawns a new bound-service + chat handle for [account], connects the
- * account on it, and registers the resulting [AccountSession] under the
- * lower-cased name. Idempotent — returns the existing session if one is
- * already registered. Both accounts stay live in libgnunetchat
- * simultaneously, mirroring messenger-gtk's two-process layout.
- */
- suspend fun spawnSessionFor(account: ChatAccount): AccountSession {
- val key = account.name.lowercase()
- sessions[key]?.let { return it }
-
- Log.d(TAG, "spawnSessionFor: starting session for '${account.name}'")
- val svc = ServiceFactory.create(applicationContext, useMock = false)
- val refreshSeen = CompletableDeferred<Unit>()
- val loginSeen = CompletableDeferred<Unit>()
-
- val newHandle = svc.startChat(MessengerApp()) { ctx, msg ->
- if (msg.kind == MessageKind.REFRESH && !refreshSeen.isCompleted) {
- refreshSeen.complete(Unit)
- }
- if (msg.kind == MessageKind.LOGIN && !loginSeen.isCompleted) {
- loginSeen.complete(Unit)
- }
- sessions[key]?.let { existing ->
- processChatMessageRouted(existing, ctx, msg)
- }
- }
-
- withTimeout(30_000) { while (newHandle.pointer == 0L) delay(50) }
- withTimeout(30_000) { refreshSeen.await() }
-
- Log.d(TAG, "spawnSessionFor: handle ready for '${account.name}', connecting")
- svc.connect(newHandle, account)
- withTimeout(20_000) { loginSeen.await() }
-
- val session = AccountSession(account, newHandle, svc)
- sessions[key] = session
- Log.d(TAG, "spawnSessionFor: '${account.name}' live (handle=${newHandle.pointer})")
- return session
- }
-
- /**
- * Routes a daemon message to [processChatMessage] only when [session] is
- * the foreground one. Background sessions still receive their events at
- * libgnunetchat layer; switching to a background session triggers a
- * loadChats() that reads the up-to-date state from the daemon.
- */
- private fun processChatMessageRouted(
- session: AccountSession,
- chatContext: ChatContext,
- chatMessage: ChatMessage
- ) {
- val current = currentSession()
- val isForeground = current != null &&
- current.account.name.equals(session.account.name, ignoreCase = true)
-
- if (isForeground) {
- processChatMessage(chatContext, chatMessage)
- return
- }
-
- // Background session: route TEXT/FILE messages into the global chat
- // viewmodel using the session's own service so they're visible when
- // the user later switches to this session. Other event kinds will be
- // re-rendered via loadChats() on switch.
- when (chatMessage.kind) {
- MessageKind.TEXT, MessageKind.FILE -> {
- val senderKey = chatMessage.sender?.key ?: ""
- val ownKey = runCatching {
- session.gnunetChat.getProfileKey(session.handle)
- }.getOrDefault("")
- if (senderKey.isNotEmpty() && senderKey == ownKey) {
- Log.d(
- TAG,
- "background-session ${session.account.name}: kind=${chatMessage.kind} " +
- "(own echo, skip)"
- )
- return
- }
- val key = stableChatKeyFor(session, chatContext)
- if (key == null) {
- Log.w(
- TAG,
- "background-session ${session.account.name}: kind=${chatMessage.kind} " +
- "no stableChatKey — dropping"
- )
- return
- }
- val vm = chatViewModels.getOrPut(key) { ChatViewModel(chatContext) }
- chatMessage.type = ChatMessageType.OTHER
- vm.addMessage(chatMessage)
- Log.d(
- TAG,
- "background-session ${session.account.name}: kind=${chatMessage.kind} " +
- "added to viewmodel '$key' (msgCount=${vm.messages.value?.size ?: 0})"
- )
- }
- else -> {
- Log.d(
- TAG,
- "background-session ${session.account.name}: kind=${chatMessage.kind} " +
- "(deferred — will re-render on next switch)"
- )
- }
- }
- }
-
- /**
- * Same as [stableChatKey] but resolves group/contact info via the given
- * [session]'s service. Required when the message arrived on a background
- * session whose native context pointer is in that session's address
- * space — the foreground service can't dereference it.
- */
- private fun stableChatKeyFor(
- session: AccountSession,
- chatContext: ChatContext
- ): String? {
- val gnunetChat = session.gnunetChat
- runCatching { gnunetChat.getGroupFromContext(chatContext) }
- .getOrNull()
- ?.takeIf { it.name.isNotBlank() }
- ?.let { return "group:${it.name}" }
-
- runCatching { gnunetChat.getContextContact(chatContext) }
- .getOrNull()
- ?.key
- ?.takeIf { it.isNotBlank() }
- ?.let { return "contact:$it" }
-
- return chatContext.userPointer?.takeIf { it.isNotBlank() }
- }
-
- /**
- * Switches the foreground UI to [account]. Spawns a new session if one
- * doesn't exist. **Never disconnects** existing sessions — both the lobby
- * host and the joiner can stay live across the switch, which is what
- * makes lobby pairing succeed for both sides on a single daemon.
- */
- suspend fun switchToSession(account: ChatAccount) {
- val key = account.name.lowercase()
- val previous = currentAccount
- val isSame = previous != null &&
- previous.name.equals(account.name, ignoreCase = true)
- if (isSame) return
-
- val session = sessions[key] ?: spawnSessionFor(account)
-
- runCatching {
- account.key = session.gnunetChat.getProfileKey(session.handle)
- }.onFailure { Log.w(TAG, "switchToSession: getProfileKey failed", it) }
-
- clearChatState()
- setCurrentAccount(account)
- runCatching { loadChatsAndWait() }
- .onFailure { Log.w(TAG, "switchToSession: loadChatsAndWait failed", it) }
- }
}
\ No newline at end of file
diff --git a/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/ui/account/AccountListFragment.kt b/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/ui/account/AccountListFragment.kt
@@ -77,13 +77,16 @@ class AccountListFragment : Fragment() {
adapter = AccountAdapter { selectedAccount ->
viewLifecycleOwner.lifecycleScope.launch {
try {
- // Multi-handle: spawn-or-reuse a per-account session. Old
- // sessions stay live in the background so a lobby host
- // survives the switch and can complete the pairing
- // handshake when the joiner arrives.
- activity.switchToSession(selectedAccount)
+ // Single-handle: connect this account on the shared chat
+ // handle. libgnunetchat handles the implicit disconnect of
+ // any previously connected account; the LOGIN message
+ // arriving via processChatMessage triggers loadChats().
+ val gnunetChat = activity.getGnunetChatInstance()
+ val handle = activity.getChatHandle()
+ activity.setCurrentAccount(selectedAccount)
+ gnunetChat.connect(handle, selectedAccount)
} catch (t: Throwable) {
- Log.e(TAG, "switchToSession failed", t)
+ Log.e(TAG, "connect failed", t)
showError(getString(R.string.account_connect_failed))
return@launch
}