messenger-android

Android graphical user interfaces for GNUnet Messenger
Log | Files | Refs | README | LICENSE

commit 51be6a213ec062dbc5576c31103f5e93ef67bcad
parent 9eb549294279c8eebded56cc94892dd364c110eb
Author: t3sserakt <t3ss@posteo.de>
Date:   Wed, 13 May 2026 14:06:33 +0200

moved to single handled application

Diffstat:
MGNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/MainActivity.kt | 241+++++++++++++------------------------------------------------------------------
MGNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/ui/account/AccountListFragment.kt | 15+++++++++------
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 }