messenger-android

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

commit 5f8c8a46753eadcfa08320ae826922325ef5bf38
parent 29dc79f8f0d0135646c43cbb8e010136c17fdd2f
Author: t3sserakt <t3sserakt@posteo.de>
Date:   Thu, 19 Mar 2026 19:49:01 +0100

UI Waiting for first account list refresh. Fixed wrong enum for message kind.

Diffstat:
MGNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/MainActivity.kt | 48+++++++++++++++++++++++++++++++++++++-----------
MGNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/model/MessageKind.kt | 14+++++++-------
MGNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/service/GnunetChat.kt | 75+++++++++++++++++++++++++++++++++++----------------------------------------
MGNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/service/boundimpl/GnunetChatBoundService.kt | 359++++++++++++++++++++++++++++++++++++-------------------------------------------
MGNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/service/mock/GnunetChatMock.kt | 157+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
MGNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/ui/account/AccountListFragment.kt | 72+++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
MGNUnetMessenger/app/src/main/res/values/strings.xml | 2++
7 files changed, 406 insertions(+), 321 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 @@ -26,6 +26,8 @@ package org.gnunet.gnunetmessenger import android.os.Bundle import android.util.Log +import android.view.Menu +import android.view.MenuItem import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar @@ -36,6 +38,8 @@ import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.NavigationUI import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import org.gnunet.gnunetmessenger.model.ChatAccount @@ -62,6 +66,8 @@ class MainActivity : AppCompatActivity() { private lateinit var handle: ChatHandle private val chatReady = CompletableDeferred<ChatHandle>() + private val initialRefreshReady = CompletableDeferred<Unit>() + private val accountRefreshEvents = MutableSharedFlow<Unit>(extraBufferCapacity = 1) private val chatOverviewViewModel: ChatOverviewViewModel by viewModels() val contactListViewModel: ContactListViewModel by viewModels() @@ -95,12 +101,15 @@ class MainActivity : AppCompatActivity() { if (!chatReady.isCompleted) { chatReady.complete(handle) } - Log.d(TAG, "Chat is ready with handle=${handle.pointer}") + Log.d(TAG, "Chat handle ready: ${handle.pointer}") } catch (t: Throwable) { - Log.e(TAG, "Chat initialization failed", t) + Log.e(TAG, "Chat initialization failed before first refresh", t) if (!chatReady.isCompleted) { chatReady.completeExceptionally(t) } + if (!initialRefreshReady.isCompleted) { + initialRefreshReady.completeExceptionally(t) + } } } @@ -131,10 +140,22 @@ class MainActivity : AppCompatActivity() { return chatReady.await() } + suspend fun awaitInitialDataReady(): ChatHandle { + val readyHandle = chatReady.await() + initialRefreshReady.await() + return readyHandle + } + + fun accountRefreshFlow(): SharedFlow<Unit> = accountRefreshEvents + fun isChatReady(): Boolean { return chatReady.isCompleted && handle.pointer != 0L } + fun hasInitialRefresh(): Boolean { + return initialRefreshReady.isCompleted + } + private fun processChatMessage(chatContext: ChatContext, chatMessage: ChatMessage) { requireNotNull(chatMessage) @@ -143,6 +164,12 @@ class MainActivity : AppCompatActivity() { } MessageKind.REFRESH -> { + Log.d(TAG, "Received REFRESH") + if (!initialRefreshReady.isCompleted) { + initialRefreshReady.complete(Unit) + Log.d(TAG, "Initial refresh barrier released") + } + accountRefreshEvents.tryEmit(Unit) } MessageKind.LOGIN -> { @@ -217,10 +244,7 @@ class MainActivity : AppCompatActivity() { val uuid = gnunetChat.getUserPointerForContext(chatContext) val localChatContext = chats[uuid] chatMessage.type = - if (gnunetChat.getContactKey(chatMessage.sender!!) == gnunetChat.getProfileKey( - handle - ) - ) { + if (gnunetChat.getContactKey(chatMessage.sender!!) == gnunetChat.getProfileKey(handle)) { ChatMessageType.OWN } else { ChatMessageType.OTHER @@ -317,7 +341,7 @@ class MainActivity : AppCompatActivity() { chatOverviewViewModel.setChats(summaries) } - override fun onCreateOptionsMenu(menu: android.view.Menu?): Boolean { + override fun onCreateOptionsMenu(menu: Menu?): Boolean { menuInflater.inflate(R.menu.main_menu, menu) val current = currentAccount menu?.findItem(R.id.menu_current_account)?.title = @@ -325,14 +349,14 @@ class MainActivity : AppCompatActivity() { return true } - override fun onPrepareOptionsMenu(menu: android.view.Menu?): Boolean { + override fun onPrepareOptionsMenu(menu: Menu?): Boolean { val current = currentAccount menu?.findItem(R.id.menu_current_account)?.title = "Account: ${current?.name ?: "No active account!"}" return super.onPrepareOptionsMenu(menu) } - override fun onOptionsItemSelected(item: android.view.MenuItem): Boolean { + override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.menu_current_account -> { if (currentAccount != null) { @@ -355,13 +379,15 @@ class MainActivity : AppCompatActivity() { } R.id.menu_new_group -> { - val action = NavGraphDirections.actionGlobalCreateGroupOrPlatformFragment(isGroup = true) + val action = + NavGraphDirections.actionGlobalCreateGroupOrPlatformFragment(isGroup = true) navController.navigate(action) true } R.id.menu_new_platform -> { - val action = NavGraphDirections.actionGlobalCreateGroupOrPlatformFragment(isGroup = false) + val action = + NavGraphDirections.actionGlobalCreateGroupOrPlatformFragment(isGroup = false) navController.navigate(action) true } diff --git a/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/model/MessageKind.kt b/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/model/MessageKind.kt @@ -25,13 +25,13 @@ package org.gnunet.gnunetmessenger.model enum class MessageKind(val code: Int) { - WARNING(0), REFRESH(1), LOGIN(2), LOGOUT(3), - CREATED_ACCOUNT(4), UPDATE_ACCOUNT(5), - UPDATE_CONTEXT(6), JOIN(7), LEAVE(8), - CONTACT(9), SHARED_ATTRIBUTES(10), - INVITATION(11), TEXT(12), FILE(13), - DELETION(14), TAG(15), ATTRIBUTES(16), - DISCOURSE(17), DATA(18); + UNKOWN (code = 0),WARNING(1), REFRESH(2), LOGIN(3), LOGOUT(4), + CREATED_ACCOUNT(5), DELETE_ACCOUNT(code = 6), UPDATE_ACCOUNT(7), + UPDATE_CONTEXT(8), JOIN(9), LEAVE(10), + CONTACT(11), + INVITATION(12), TEXT(13), FILE(14), + DELETION(15), TAG(16), ATTRIBUTES(17), SHARED_ATTRIBUTES(18), + DISCOURSE(19), DATA(20); companion object { fun fromCode(code: Int): MessageKind = diff --git a/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/service/GnunetChat.kt b/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/service/GnunetChat.kt @@ -1,30 +1,5 @@ -/* - This file is part of GNUnet. - Copyright (C) 2021--2025 GNUnet e.V. - - GNUnet is free software: you can redistribute it and/or modify it - under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, - or (at your option) any later version. - - GNUnet is distributed in the hope that it will be useful, but - WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see <http://www.gnu.org/licenses/>. - - SPDX-License-Identifier: AGPL3.0-or-later - */ -/* - * @author t3sserakt - * @file GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/service/GnunetChat.kt - */ - package org.gnunet.gnunetmessenger.service - import org.gnunet.gnunetmessenger.model.ChatAccount import org.gnunet.gnunetmessenger.model.ChatContact import org.gnunet.gnunetmessenger.model.ChatContext @@ -37,53 +12,72 @@ import org.gnunet.gnunetmessenger.model.MessageKind import org.gnunet.gnunetmessenger.model.MessengerApp interface GnunetChat { + fun startChat( + messengerApp: MessengerApp, + callback: (ChatContext, ChatMessage) -> Unit + ): ChatHandle + suspend fun awaitReady(handle: ChatHandle) - fun startChat(messengerApp: MessengerApp, callback: (ChatContext, ChatMessage) -> Unit): ChatHandle + suspend fun reset() + fun iterateAccounts(handle: ChatHandle, callback: (ChatAccount) -> Unit) + suspend fun listAccounts(handle: ChatHandle): List<ChatAccount> + suspend fun createAccount(handle: ChatHandle, name: String): GnunetReturnValue suspend fun connect(handle: ChatHandle, account: ChatAccount) suspend fun disconnect(handle: ChatHandle) suspend fun getProfileName(handle: ChatHandle): String suspend fun setProfileName(handle: ChatHandle, name: String) + fun getProfileKey(handle: ChatHandle): String - fun setContactBlocked(contact: ChatContact, isBlocked: Boolean) fun isContactBlocked(contact: ChatContact): Boolean - fun setAttribute(handle: ChatHandle,key: String, value: String) + fun setContactBlocked(contact: ChatContact, isBlocked: Boolean) + fun setAttribute(handle: ChatHandle, key: String, value: String) fun getAttributes(handle: ChatHandle, callback: (String, String) -> Unit) + fun lobbyOpen(handle: ChatHandle, callback: (String) -> Unit) fun lobbyJoin(handle: ChatHandle, uri: String) + fun setGroupName(group: ChatGroup, name: String) fun createGroup(handle: ChatHandle, topic: String): ChatGroup + fun parseUri(uri: String): ChatUri fun destroyUri(uri: ChatUri) + fun inviteContactToGroup(group: ChatGroup, contact: ChatContact) - fun getUserPointerForContext(context: ChatContext) : String? + fun getUserPointerForContext(context: ChatContext): String? fun setUserPointerForContext(context: ChatContext, userPointer: String) - fun getSenderFromMessage(message: ChatMessage) : ChatContact + + fun getSenderFromMessage(message: ChatMessage): ChatContact fun getGroupFromContext(context: ChatContext): ChatGroup? - fun getMessageForGroupContact(group: ChatGroup, contact: ChatContact) : ChatMessage - fun getMessageKind(message: ChatMessage) : MessageKind - fun isMessageRecent(message: ChatMessage) : GnunetReturnValue - fun getMessageTimestamp(message: ChatMessage) : Long + fun getMessageForGroupContact(group: ChatGroup, contact: ChatContact): ChatMessage + fun getMessageKind(message: ChatMessage): MessageKind + fun isMessageRecent(message: ChatMessage): GnunetReturnValue + fun getMessageTimestamp(message: ChatMessage): Long fun setMessageForGroupContact(group: ChatGroup, contact: ChatContact, message: ChatMessage) + fun iterateContacts(handle: ChatHandle, callback: (ChatContact) -> Int) fun iterateGroups(handle: ChatHandle, callback: (ChatGroup) -> Int) + fun getContactContext(chatContact: ChatContact): ChatContext - fun getGroupContext(chatGroup: ChatGroup): ChatContext - fun getContactUserPointer(chatContact: ChatContact) : String + fun getGroupContext(chatGroup: ChatGroup): ChatContext + fun getContactUserPointer(chatContact: ChatContact): String fun setContactUserPointer(chatContact: ChatContact, userPointer: String) - fun getGroupUserPointer(chatGroup: ChatGroup) : String + fun getGroupUserPointer(chatGroup: ChatGroup): String fun setGroupUserPointer(chatGroup: ChatGroup, userPointer: String) + fun sendText(chatContext: ChatContext, text: String) fun getContactKey(chatContact: ChatContact): String fun getContextContact(context: ChatContext): ChatContact fun deleteContact(chatContact: ChatContact) fun isGroup(context: ChatContext): Boolean fun isPlatform(context: ChatContext): Boolean + fun iterateGroupContacts(chatGroup: ChatGroup, callback: (ChatGroup, ChatContact) -> Int) + fun randomUUID(): String fun getContactAttributes(contact: ChatContact, callback: (String, String) -> Unit) - fun shareAttributes (handle: ChatHandle, contact: ChatContact, key: String) - fun unshareAttributes (handle: ChatHandle, contact: ChatContact, key: String) -} + fun shareAttributes(handle: ChatHandle, contact: ChatContact, key: String) + fun unshareAttributes(handle: ChatHandle, contact: ChatContact, key: String) +} +\ No newline at end of file diff --git a/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/service/boundimpl/GnunetChatBoundService.kt b/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/service/boundimpl/GnunetChatBoundService.kt @@ -4,8 +4,8 @@ import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.ServiceConnection -import android.os.IBinder import android.os.DeadObjectException +import android.os.IBinder import android.os.RemoteException import android.util.Log import kotlinx.coroutines.CompletableDeferred @@ -16,7 +16,6 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext -import kotlinx.coroutines.withTimeout import org.gnunet.gnunetmessenger.ipc.* import org.gnunet.gnunetmessenger.model.* import org.gnunet.gnunetmessenger.service.GnunetChat @@ -29,49 +28,41 @@ class GnunetChatBoundService( private var uuidCounter: Long = 0 - // --- Scopes --- private val mainScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) private val ioScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - // --- Binder state --- private val remoteRef = AtomicReference<IGnunetChat?>() private var deathRecipient: IBinder.DeathRecipient? = null - @Volatile private var lastHandle: Long = 0L // echter Server-Handle - @Volatile private lateinit var messageCallback: ((ChatContext, ChatMessage) -> Unit) - - // Aufgaben, die erst NACH erfolgreichem startChat(handle!=0) ausgeführt werden dürfen - private val pendingAfterHandle = mutableListOf<(IGnunetChat, Long) -> Unit>() - - private data class PendingStart(val appName: String, val ch: ChatHandle) + @Volatile + private var lastHandle: Long = 0L @Volatile - private var pendingStart: PendingStart? = null + private lateinit var messageCallback: ((ChatContext, ChatMessage) -> Unit) + + private val pendingAfterHandle = mutableListOf<(IGnunetChat, Long) -> Unit>() private val handleReady = ConcurrentHashMap<ChatHandle, CompletableDeferred<Long>>() - // ----- Callback vom Server (AIDL) -> App-Callback ----- private val binderCallback = object : IChatCallback.Stub() { override fun onMessage(context: ChatContextDto, message: ChatMessageDto) { try { val ctxLocal = context.toLocal() val msgLocal = message.toLocal(ctxLocal) - mainScope.launch { messageCallback?.invoke(ctxLocal, msgLocal) } + mainScope.launch { messageCallback.invoke(ctxLocal, msgLocal) } } catch (t: Throwable) { Log.e(TAG, "onMessage mapping failed", t) } } } - // ----- ServiceConnection ----- private val conn = object : ServiceConnection { override fun onServiceConnected(name: ComponentName, service: IBinder) { val remote = IGnunetChat.Stub.asInterface(service) remoteRef.set(remote) - // Death-Handling val dr = IBinder.DeathRecipient { Log.w(TAG, "Remote binder died") remoteRef.set(null) @@ -81,24 +72,6 @@ class GnunetChatBoundService( deathRecipient = dr runCatching { service.linkToDeath(dr, 0) } .onFailure { Log.e(TAG, "linkToDeath failed", it) } - - // Falls der App-Callback schon gesetzt wurde, aber noch kein Handle existiert: - if (::messageCallback.isInitialized && lastHandle == 0L) { - ioScope.launch { - try { - val h = remote.startChat(DEFAULT_APP_NAME, binderCallback) - Log.d(TAG, "startChat after bind -> handle=$h") - lastHandle = h - // Alle auf Handle wartenden Aufgaben ausführen - drainPending(remote, h) - // Falls wir dem Aufrufer schon einen Platzhalter-ChatHandle gegeben haben, - // aktualisiert er dessen pointer in startChat() (s.u.) - // -> hier nichts weiter nötig. - } catch (e: RemoteException) { - Log.e(TAG, "startChat after bind failed", e) - } - } - } } override fun onServiceDisconnected(name: ComponentName) { @@ -109,7 +82,6 @@ class GnunetChatBoundService( } init { - // proaktiv binden (damit es später schneller geht) bind() } @@ -130,30 +102,30 @@ class GnunetChatBoundService( retries: Int = 1, crossinline block: suspend (IGnunetChat, Long) -> T ): T { - // stellt sicher: gültiger Handle + verbundener Remote awaitReady(handle) var attempt = 0 var lastError: Throwable? = null while (attempt <= retries) { - val remote = try { getOrBindRemote() } catch (t: Throwable) { - lastError = t; null + val remote = try { + getOrBindRemote() + } catch (t: Throwable) { + lastError = t + null } + if (remote != null) { try { return block(remote, handle.pointer) - } catch (dead: android.os.DeadObjectException) { - // Binder tot -> rebind und retry + } catch (dead: DeadObjectException) { Log.w(TAG, "Binder died, rebinding… (attempt=$attempt)") bind() lastError = dead } catch (re: RemoteException) { - // manche RemoteExceptions bedeuten auch: Binder tot Log.w(TAG, "RemoteException, retry if possible (attempt=$attempt)", re) bind() lastError = re } } else { - // keine Verbindung -> kurze Pause vorm Retry delay(150) } attempt++ @@ -181,60 +153,38 @@ class GnunetChatBoundService( } override suspend fun awaitReady(handle: ChatHandle) { - // Falls der echte Pointer schon gesetzt ist: sofort fertig. if (handle.pointer != 0L) return - // Gibt es ein Deferred für genau DIESEN Handle? (wird in startChat angelegt) handleReady[handle]?.let { deferred -> try { - val h = deferred.await() // suspendiert, blockiert NICHT den Main-Thread + val h = deferred.await() if (handle.pointer == 0L) { - handle.pointer = h // zur Sicherheit setzen, falls Racing + handle.pointer = h } return } finally { - // Aufräumen: Eintrag entfernen (verhindert Leaks). handleReady.remove(handle) } } - // Kein Deferred gefunden. War der Remote evtl. noch nicht verbunden? - // Warte kurz auf die Verbindung (max. ~2s). repeat(20) { if (remoteRef.get() != null) return@repeat delay(100) } + val remote = remoteRef.get() - ?: throw IllegalStateException("Remote not connected; startChat/bind() noch nicht durch") - - // Wenn wir bis hier kamen und der Pointer immer noch 0 ist: - // Prüfe, ob ein ausstehender startChat für GENAU diesen Handle vormerkt ist. - pendingStart?.let { ps -> - val (appName, ch) = ps - if (ch === handle) { - // Wir waren gebunden, aber startChat wurde noch nicht abgesetzt – hole das nach. - val real = remote.startChat(appName, binderCallback) - handle.pointer = real - handleReady[handle]?.complete(real) - pendingStart = null - handleReady.remove(handle) - return - } - } + ?: throw IllegalStateException("Remote not connected; startChat/bind() not completed") - // Letzter Versuch: vielleicht wurde kurz vorher doch noch ein Deferred angelegt. - handleReady[handle]?.let { deferred -> - try { - val h = deferred.await() - if (handle.pointer == 0L) handle.pointer = h - return - } finally { - handleReady.remove(handle) - } + if (handle.pointer == 0L) { + val real = remote.startChat(DEFAULT_APP_NAME, binderCallback) + handle.pointer = real + lastHandle = real + handleReady[handle]?.complete(real) + handleReady.remove(handle) + drainPending(remote, real) } - // Wenn wir hier landen, ist der Handle immer noch 0 → sauber fehlschlagen. - throw IllegalStateException("Handle not ready (pointer==0); startChat() wurde evtl. nie erfolgreich abgeschlossen") + check(handle.pointer != 0L) { "Handle not ready (pointer==0)" } } override fun startChat( @@ -243,36 +193,22 @@ class GnunetChatBoundService( ): ChatHandle { messageCallback = callback - // Platzhalter-Handle sofort zurückgeben (UI blockiert nicht) val ch = ChatHandle(0L) + val deferred = CompletableDeferred<Long>() + handleReady[ch] = deferred - val remote = remoteRef.get() - if (remote != null) { - // Wir sind gebunden -> Session starten (auch wenn lastHandle == 0L nach reset) - ioScope.launch { - runCatching { remote.startChat("messengerApp", binderCallback) } - .onSuccess { h -> - lastHandle = h - ch.pointer = h // << echten Handle in deinen ChatHandle schreiben - Log.d(TAG, "startChat -> handle=$h") - drainPending(remote, h) // << aufgestaute Calls abarbeiten - } - .onFailure { t -> - Log.e(TAG, "startChat failed", t) - } - } - } else { - // Noch nicht gebunden: Bind anstoßen; onServiceConnected ruft startChat - bind() - - // Wir müssen den Platzhalter später noch mit dem echten Handle füllen: - // Das erledigen wir, indem wir eine "No-Op"-Pending-Task eintragen, - // die NUR den ChatHandle aktualisiert, sobald der echte Handle da ist. - synchronized(pendingAfterHandle) { - pendingAfterHandle += { _, handleReal -> - ch.pointer = handleReal - Log.d(TAG, "late handle propagation -> ${ch.pointer}") - } + ioScope.launch { + try { + val remote = getOrBindRemote() + val h = remote.startChat("messengerApp", binderCallback) + lastHandle = h + ch.pointer = h + deferred.complete(h) + drainPending(remote, h) + Log.d(TAG, "startChat -> handle=$h") + } catch (t: Throwable) { + Log.e(TAG, "startChat failed", t) + deferred.completeExceptionally(t) } } @@ -280,26 +216,14 @@ class GnunetChatBoundService( } override suspend fun reset() = withContext(Dispatchers.IO) { - val remote = try { - getOrBindRemote() - } catch (t: Throwable) { - Log.e(TAG, "reset: failed to bind remote", t) - throw t - } - - try { - remote.reset() - Log.i(TAG, "reset: successfully reset remote service") - - // Reset local state - lastHandle = 0L - handleReady.clear() - synchronized(pendingAfterHandle) { - pendingAfterHandle.clear() - } - } catch (e: RemoteException) { - Log.e(TAG, "reset: remote call failed", e) - throw e + val remote = getOrBindRemote() + remote.reset() + Log.i(TAG, "reset: successfully reset remote service") + + lastHandle = 0L + handleReady.clear() + synchronized(pendingAfterHandle) { + pendingAfterHandle.clear() } } @@ -309,7 +233,11 @@ class GnunetChatBoundService( val acc = accountDto.toLocal() mainScope.launch { callback(acc) } } - override fun onDone() { Log.d(TAG, "iterateAccounts: done") } + + override fun onDone() { + Log.d(TAG, "iterateAccounts: done") + } + override fun onError(code: Int, message: String?) { Log.e(TAG, "iterateAccounts: error $code $message") } @@ -323,23 +251,25 @@ class GnunetChatBoundService( ioScope.launch { try { remote.iterateAccounts(h, bridge) - } catch (dead: android.os.DeadObjectException) { - // Binder tot -> re-queue und rebind + } catch (dead: DeadObjectException) { Log.w(TAG, "iterateAccounts: binder died, queue & rebind") synchronized(pendingAfterHandle) { pendingAfterHandle += { r, real -> runCatching { r.iterateAccounts(real, bridge) } - .onFailure { Log.e(TAG, "iterateAccounts (deferred) failed", it) } + .onFailure { + Log.e(TAG, "iterateAccounts (deferred) failed", it) + } } } bind() } catch (e: RemoteException) { Log.e(TAG, "iterateAccounts remote failed", e) - // sicherheitshalber auch re-queue synchronized(pendingAfterHandle) { pendingAfterHandle += { r, real -> runCatching { r.iterateAccounts(real, bridge) } - .onFailure { Log.e(TAG, "iterateAccounts (deferred) failed", it) } + .onFailure { + Log.e(TAG, "iterateAccounts (deferred) failed", it) + } } } bind() @@ -347,7 +277,6 @@ class GnunetChatBoundService( } } - // Verbunden, aber Handle noch nicht bereit → aufschieben bis handle da ist remote != null && h == 0L -> { synchronized(pendingAfterHandle) { pendingAfterHandle += { r, real -> @@ -357,7 +286,6 @@ class GnunetChatBoundService( } } - // Noch nicht verbunden → binden & aufschieben else -> { synchronized(pendingAfterHandle) { pendingAfterHandle += { r, real -> @@ -370,7 +298,36 @@ class GnunetChatBoundService( } } - // --- Helpers --- + override suspend fun listAccounts(handle: ChatHandle): List<ChatAccount> { + return withReadyRemote(handle) { remote, h -> + val result = mutableListOf<ChatAccount>() + val done = CompletableDeferred<Unit>() + + val bridge = object : IAccountCallback.Stub() { + override fun onAccount(accountDto: ChatAccountDto) { + result.add(accountDto.toLocal()) + } + + override fun onDone() { + if (!done.isCompleted) { + done.complete(Unit) + } + } + + override fun onError(code: Int, message: String?) { + if (!done.isCompleted) { + done.completeExceptionally( + IllegalStateException("iterateAccounts failed: $code ${message ?: ""}".trim()) + ) + } + } + } + + remote.iterateAccounts(h, bridge) + done.await() + result.toList() + } + } private fun drainPending(remote: IGnunetChat, handle: Long) { val tasks = synchronized(pendingAfterHandle) { @@ -387,26 +344,12 @@ class GnunetChatBoundService( } } - private suspend fun resolveHandle(h: ChatHandle): Long { - if (h.pointer != 0L) return h.pointer - handleReady[h]?.let { return it.await() } - if (lastHandle != 0L) return lastHandle - throw IllegalStateException("No handle available yet (startChat not completed)") - } - - // int <-> enum mapping private fun Int.toGnunetReturn(): GnunetReturnValue = when (this) { 0 -> GnunetReturnValue.OK else -> GnunetReturnValue.NO } - private fun Int.toReturnValue(): GnunetReturnValue = - when (this) { - 0 -> GnunetReturnValue.OK - else -> GnunetReturnValue.NO // oder ein feineres Mapping, falls vorhanden - } - override suspend fun createAccount(handle: ChatHandle, name: String): GnunetReturnValue { val code = withReadyRemote(handle) { remote, h -> withContext(Dispatchers.IO) { remote.createAccount(h, name) } @@ -441,8 +384,6 @@ class GnunetChatBoundService( } } - // --- Remote Call Implementations --- - override fun getProfileKey(handle: ChatHandle): String { return runBlocking { withReadyRemote(handle) { remote, h -> @@ -453,7 +394,7 @@ class GnunetChatBoundService( override fun isContactBlocked(contact: ChatContact): Boolean { return runBlocking { - withReadyRemote(ChatHandle(0)) { remote, h -> + withReadyRemote(ChatHandle(0)) { remote, _ -> remote.isContactBlocked(contact.toDto()) } } @@ -461,7 +402,7 @@ class GnunetChatBoundService( override fun setContactBlocked(contact: ChatContact, isBlocked: Boolean) { runBlocking { - withReadyRemote(ChatHandle(0)) { remote, h -> + withReadyRemote(ChatHandle(0)) { remote, _ -> remote.setContactBlocked(contact.toDto(), isBlocked) } } @@ -480,7 +421,11 @@ class GnunetChatBoundService( override fun onAttribute(key: String, value: String) { mainScope.launch { callback(key, value) } } - override fun onDone() { Log.d(TAG, "getAttributes: done") } + + override fun onDone() { + Log.d(TAG, "getAttributes: done") + } + override fun onError(code: Int, message: String?) { Log.e(TAG, "getAttributes: error $code $message") } @@ -511,6 +456,7 @@ class GnunetChatBoundService( override fun onLobbyUri(uri: String) { mainScope.launch { callback(uri) } } + override fun onError(code: Int, message: String?) { Log.e(TAG, "lobbyOpen: error $code $message") } @@ -546,7 +492,7 @@ class GnunetChatBoundService( override fun setGroupName(group: ChatGroup, name: String) { runBlocking { - withReadyRemote(ChatHandle(0)) { remote, h -> + withReadyRemote(ChatHandle(0)) { remote, _ -> remote.setGroupName(group.toDto(), name) } } @@ -563,7 +509,7 @@ class GnunetChatBoundService( override fun parseUri(uri: String): ChatUri { return runBlocking { - val uriDto = withReadyRemote(ChatHandle(0)) { remote, h -> + val uriDto = withReadyRemote(ChatHandle(0)) { remote, _ -> remote.parseUri(uri) } uriDto.toLocal() @@ -572,7 +518,7 @@ class GnunetChatBoundService( override fun destroyUri(uri: ChatUri) { runBlocking { - withReadyRemote(ChatHandle(0)) { remote, h -> + withReadyRemote(ChatHandle(0)) { remote, _ -> remote.destroyUri(uri.toDto()) } } @@ -580,7 +526,7 @@ class GnunetChatBoundService( override fun inviteContactToGroup(group: ChatGroup, contact: ChatContact) { runBlocking { - withReadyRemote(ChatHandle(0)) { remote, h -> + withReadyRemote(ChatHandle(0)) { remote, _ -> remote.inviteContactToGroup(group.toDto(), contact.toDto()) } } @@ -588,7 +534,7 @@ class GnunetChatBoundService( override fun getUserPointerForContext(context: ChatContext): String? { return runBlocking { - withReadyRemote(ChatHandle(0)) { remote, h -> + withReadyRemote(ChatHandle(0)) { remote, _ -> remote.getUserPointerForContext(context.toDto()) } } @@ -596,7 +542,7 @@ class GnunetChatBoundService( override fun setUserPointerForContext(context: ChatContext, userPointer: String) { runBlocking { - withReadyRemote(ChatHandle(0)) { remote, h -> + withReadyRemote(ChatHandle(0)) { remote, _ -> remote.setUserPointerForContext(context.toDto(), userPointer) } } @@ -604,7 +550,7 @@ class GnunetChatBoundService( override fun getSenderFromMessage(message: ChatMessage): ChatContact { return runBlocking { - val contactDto = withReadyRemote(ChatHandle(0)) { remote, h -> + val contactDto = withReadyRemote(ChatHandle(0)) { remote, _ -> remote.getSenderFromMessage(message.toDto()) } contactDto.toLocal() @@ -613,7 +559,7 @@ class GnunetChatBoundService( override fun getGroupFromContext(context: ChatContext): ChatGroup? { return runBlocking { - val groupDto = withReadyRemote(ChatHandle(0)) { remote, h -> + val groupDto = withReadyRemote(ChatHandle(0)) { remote, _ -> remote.getGroupFromContext(context.toDto()) } groupDto.toLocal() @@ -622,16 +568,16 @@ class GnunetChatBoundService( override fun getMessageForGroupContact(group: ChatGroup, contact: ChatContact): ChatMessage { return runBlocking { - val messageDto = withReadyRemote(ChatHandle(0)) { remote, h -> + val messageDto = withReadyRemote(ChatHandle(0)) { remote, _ -> remote.getMessageForGroupContact(group.toDto(), contact.toDto()) } - messageDto.toLocal(ChatContext(ChatContextType.UNKNOWN, null, false, false)) + messageDto.toLocal(ChatContext(null, null, false, false)) } } override fun getMessageKind(message: ChatMessage): MessageKind { return runBlocking { - val kind = withReadyRemote(ChatHandle(0)) { remote, h -> + val kind = withReadyRemote(ChatHandle(0)) { remote, _ -> remote.getMessageKind(message.toDto()) } MessageKind.fromCode(kind) @@ -640,7 +586,7 @@ class GnunetChatBoundService( override fun isMessageRecent(message: ChatMessage): GnunetReturnValue { return runBlocking { - val result = withReadyRemote(ChatHandle(0)) { remote, h -> + val result = withReadyRemote(ChatHandle(0)) { remote, _ -> remote.isMessageRecent(message.toDto()) } result.toGnunetReturn() @@ -649,15 +595,19 @@ class GnunetChatBoundService( override fun getMessageTimestamp(message: ChatMessage): Long { return runBlocking { - withReadyRemote(ChatHandle(0)) { remote, h -> + withReadyRemote(ChatHandle(0)) { remote, _ -> remote.getMessageTimestamp(message.toDto()) } } } - override fun setMessageForGroupContact(group: ChatGroup, contact: ChatContact, message: ChatMessage) { + override fun setMessageForGroupContact( + group: ChatGroup, + contact: ChatContact, + message: ChatMessage + ) { runBlocking { - withReadyRemote(ChatHandle(0)) { remote, h -> + withReadyRemote(ChatHandle(0)) { remote, _ -> remote.setMessageForGroupContact(group.toDto(), contact.toDto(), message.toDto()) } } @@ -669,7 +619,11 @@ class GnunetChatBoundService( val contact = contactDto.toLocal() mainScope.launch { callback(contact) } } - override fun onDone() { Log.d(TAG, "iterateContacts: done") } + + override fun onDone() { + Log.d(TAG, "iterateContacts: done") + } + override fun onError(code: Int, message: String?) { Log.e(TAG, "iterateContacts: error $code $message") } @@ -701,7 +655,11 @@ class GnunetChatBoundService( val group = groupDto.toLocal() mainScope.launch { callback(group) } } - override fun onDone() { Log.d(TAG, "iterateGroups: done") } + + override fun onDone() { + Log.d(TAG, "iterateGroups: done") + } + override fun onError(code: Int, message: String?) { Log.e(TAG, "iterateGroups: error $code $message") } @@ -729,7 +687,7 @@ class GnunetChatBoundService( override fun getContactContext(chatContact: ChatContact): ChatContext { return runBlocking { - val contextDto = withReadyRemote(ChatHandle(0)) { remote, h -> + val contextDto = withReadyRemote(ChatHandle(0)) { remote, _ -> remote.getContactContext(chatContact.toDto()) } contextDto.toLocal() @@ -738,7 +696,7 @@ class GnunetChatBoundService( override fun getGroupContext(chatGroup: ChatGroup): ChatContext { return runBlocking { - val contextDto = withReadyRemote(ChatHandle(0)) { remote, h -> + val contextDto = withReadyRemote(ChatHandle(0)) { remote, _ -> remote.getGroupContext(chatGroup.toDto()) } contextDto.toLocal() @@ -747,7 +705,7 @@ class GnunetChatBoundService( override fun getContactUserPointer(chatContact: ChatContact): String { return runBlocking { - withReadyRemote(ChatHandle(0)) { remote, h -> + withReadyRemote(ChatHandle(0)) { remote, _ -> remote.getContactUserPointer(chatContact.toDto()) } } @@ -755,7 +713,7 @@ class GnunetChatBoundService( override fun setContactUserPointer(chatContact: ChatContact, userPointer: String) { runBlocking { - withReadyRemote(ChatHandle(0)) { remote, h -> + withReadyRemote(ChatHandle(0)) { remote, _ -> remote.setContactUserPointer(chatContact.toDto(), userPointer) } } @@ -763,7 +721,7 @@ class GnunetChatBoundService( override fun getGroupUserPointer(chatGroup: ChatGroup): String { return runBlocking { - withReadyRemote(ChatHandle(0)) { remote, h -> + withReadyRemote(ChatHandle(0)) { remote, _ -> remote.getGroupUserPointer(chatGroup.toDto()) } } @@ -771,7 +729,7 @@ class GnunetChatBoundService( override fun setGroupUserPointer(chatGroup: ChatGroup, userPointer: String) { runBlocking { - withReadyRemote(ChatHandle(0)) { remote, h -> + withReadyRemote(ChatHandle(0)) { remote, _ -> remote.setGroupUserPointer(chatGroup.toDto(), userPointer) } } @@ -779,7 +737,7 @@ class GnunetChatBoundService( override fun sendText(chatContext: ChatContext, text: String) { runBlocking { - withReadyRemote(ChatHandle(0)) { remote, h -> + withReadyRemote(ChatHandle(0)) { remote, _ -> remote.sendText(chatContext.toDto(), text) } } @@ -787,7 +745,7 @@ class GnunetChatBoundService( override fun getContactKey(chatContact: ChatContact): String { return runBlocking { - withReadyRemote(ChatHandle(0)) { remote, h -> + withReadyRemote(ChatHandle(0)) { remote, _ -> remote.getContactKey(chatContact.toDto()) } } @@ -795,7 +753,7 @@ class GnunetChatBoundService( override fun getContextContact(context: ChatContext): ChatContact { return runBlocking { - val contactDto = withReadyRemote(ChatHandle(0)) { remote, h -> + val contactDto = withReadyRemote(ChatHandle(0)) { remote, _ -> remote.getContextContact(context.toDto()) } contactDto.toLocal() @@ -804,7 +762,7 @@ class GnunetChatBoundService( override fun deleteContact(chatContact: ChatContact) { runBlocking { - withReadyRemote(ChatHandle(0)) { remote, h -> + withReadyRemote(ChatHandle(0)) { remote, _ -> remote.deleteContact(chatContact.toDto()) } } @@ -812,7 +770,7 @@ class GnunetChatBoundService( override fun isGroup(context: ChatContext): Boolean { return runBlocking { - withReadyRemote(ChatHandle(0)) { remote, h -> + withReadyRemote(ChatHandle(0)) { remote, _ -> remote.isGroup(context.toDto()) } } @@ -820,20 +778,27 @@ class GnunetChatBoundService( override fun isPlatform(context: ChatContext): Boolean { return runBlocking { - withReadyRemote(ChatHandle(0)) { remote, h -> + withReadyRemote(ChatHandle(0)) { remote, _ -> remote.isPlatform(context.toDto()) } } } - override fun iterateGroupContacts(chatGroup: ChatGroup, callback: (ChatGroup, ChatContact) -> Int) { + override fun iterateGroupContacts( + chatGroup: ChatGroup, + callback: (ChatGroup, ChatContact) -> Int + ) { val bridge = object : IGroupContactCallback.Stub() { override fun onGroupContact(groupDto: ChatGroupDto, contactDto: ChatContactDto) { val group = groupDto.toLocal() val contact = contactDto.toLocal() mainScope.launch { callback(group, contact) } } - override fun onDone() { Log.d(TAG, "iterateGroupContacts: done") } + + override fun onDone() { + Log.d(TAG, "iterateGroupContacts: done") + } + override fun onError(code: Int, message: String?) { Log.e(TAG, "iterateGroupContacts: error $code $message") } @@ -851,20 +816,18 @@ class GnunetChatBoundService( } else { bind() synchronized(pendingAfterHandle) { - pendingAfterHandle += { r, real -> + pendingAfterHandle += { r, _ -> runCatching { r.iterateGroupContacts(chatGroup.toDto(), bridge) } - .onFailure { Log.e(TAG, "iterateGroupContacts (deferred) failed", it) } + .onFailure { + Log.e(TAG, "iterateGroupContacts (deferred) failed", it) + } } } } } override fun randomUUID(): String { - return runBlocking { - withReadyRemote(ChatHandle(0)) { remote, h -> - remote.randomUUID() - } - } + return "uuid_${System.currentTimeMillis()}_${uuidCounter++}" } override fun getContactAttributes(contact: ChatContact, callback: (String, String) -> Unit) { @@ -872,7 +835,11 @@ class GnunetChatBoundService( override fun onAttribute(key: String, value: String) { mainScope.launch { callback(key, value) } } - override fun onDone() { Log.d(TAG, "getContactAttributes: done") } + + override fun onDone() { + Log.d(TAG, "getContactAttributes: done") + } + override fun onError(code: Int, message: String?) { Log.e(TAG, "getContactAttributes: error $code $message") } @@ -890,9 +857,11 @@ class GnunetChatBoundService( } else { bind() synchronized(pendingAfterHandle) { - pendingAfterHandle += { r, real -> + pendingAfterHandle += { r, _ -> runCatching { r.getContactAttributes(contact.toDto(), bridge) } - .onFailure { Log.e(TAG, "getContactAttributes (deferred) failed", it) } + .onFailure { + Log.e(TAG, "getContactAttributes (deferred) failed", it) + } } } } @@ -921,5 +890,4 @@ class GnunetChatBoundService( private const val SERVER_PACKAGE = "org.gnu.gnunet" private const val DEFAULT_APP_NAME = "Default" } - -} +} +\ No newline at end of file diff --git a/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/service/mock/GnunetChatMock.kt b/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/service/mock/GnunetChatMock.kt @@ -24,7 +24,6 @@ package org.gnunet.gnunetmessenger.service.mock - import org.gnunet.gnunetmessenger.model.ChatAccount import org.gnunet.gnunetmessenger.model.ChatContact import org.gnunet.gnunetmessenger.model.ChatContext @@ -70,9 +69,9 @@ class GnunetChatMock : GnunetChat { override fun iterateAccounts(handle: ChatHandle, callback: (ChatAccount) -> Unit) { val mockAccounts = listOf( - ChatAccount(1, "Alice",""), - ChatAccount(2, "Bob",""), - ChatAccount(3, "Charlie","") + ChatAccount(1, "Alice", ""), + ChatAccount(2, "Bob", ""), + ChatAccount(3, "Charlie", "") ) for (account in mockAccounts) { @@ -80,6 +79,14 @@ class GnunetChatMock : GnunetChat { } } + override suspend fun listAccounts(handle: ChatHandle): List<ChatAccount> { + return listOf( + ChatAccount(1, "Alice", ""), + ChatAccount(2, "Bob", ""), + ChatAccount(3, "Charlie", "") + ) + } + override suspend fun createAccount(handle: ChatHandle, name: String): GnunetReturnValue { println("create account") return GnunetReturnValue.OK @@ -87,16 +94,32 @@ class GnunetChatMock : GnunetChat { override suspend fun connect(handle: ChatHandle, account: ChatAccount) { println("connect") - messageCallback(ChatContext(ChatContextType.UNKNOWN,UUID.randomUUID().toString(), false, false), - ChatMessage(ChatContext(ChatContextType.UNKNOWN,null, true, false),"",0,null,MessageKind.LOGIN,null) + messageCallback( + ChatContext(ChatContextType.UNKNOWN, UUID.randomUUID().toString(), false, false), + ChatMessage( + ChatContext(ChatContextType.UNKNOWN, null, true, false), + "", + 0, + null, + MessageKind.LOGIN, + null + ) ) } override suspend fun disconnect(handle: ChatHandle) { println("disconnect") uuidCounter = 0 - messageCallback(ChatContext(ChatContextType.UNKNOWN,UUID.randomUUID().toString(), false, false), - ChatMessage(ChatContext(ChatContextType.UNKNOWN,null, false, false),"",0,null,MessageKind.LOGOUT,null) + messageCallback( + ChatContext(ChatContextType.UNKNOWN, UUID.randomUUID().toString(), false, false), + ChatMessage( + ChatContext(ChatContextType.UNKNOWN, null, false, false), + "", + 0, + null, + MessageKind.LOGOUT, + null + ) ) } @@ -117,7 +140,7 @@ class GnunetChatMock : GnunetChat { } override fun setContactBlocked(contact: ChatContact, isBlocked: Boolean) { - println("isblocked:" + isBlocked) + println("isblocked:$isBlocked") } override fun setAttribute(handle: ChatHandle, key: String, value: String) { @@ -140,10 +163,7 @@ class GnunetChatMock : GnunetChat { callback("000G006K2TJNMD9VTCYRX7BRVV3HAEPS15E6NHDXKPJA1KAJJEG9AFF884") } - override fun lobbyJoin( - handle: ChatHandle, - uri: String - ) { + override fun lobbyJoin(handle: ChatHandle, uri: String) { println("join lobby") } @@ -152,18 +172,17 @@ class GnunetChatMock : GnunetChat { } override fun createGroup(handle: ChatHandle, topic: String): ChatGroup { - return ChatGroup(ChatContext(ChatContextType.GROUP, null, - true, false), topic) + return ChatGroup( + ChatContext(ChatContextType.GROUP, null, true, false), + topic + ) } override fun parseUri(uri: String): ChatUri { - // Return a fake, hard-coded ChatUri. - // We'll just put the URI we received inside it. return ChatUri(uri) } override fun destroyUri(uri: ChatUri) { - // This is our "side effect" that the test can check lastDestroyedUri = uri } @@ -180,11 +199,14 @@ class GnunetChatMock : GnunetChat { } override fun getSenderFromMessage(message: ChatMessage): ChatContact { - return ChatContact(ChatContext(ChatContextType.CONTACT, null, true, false), message.sender?.name ?: "") + return ChatContact( + ChatContext(ChatContextType.CONTACT, null, true, false), + message.sender?.name ?: "" + ) } override fun getGroupFromContext(context: ChatContext): ChatGroup? { - if ("3" == context.userPointer){ + if ("3" == context.userPointer) { val contextDev = ChatContext(ChatContextType.GROUP, null, true, false) return ChatGroup(contextDev, name = "Dev Team") } @@ -193,7 +215,6 @@ class GnunetChatMock : GnunetChat { } override fun getMessageForGroupContact(group: ChatGroup, contact: ChatContact): ChatMessage { - // Return a fixed, hard-coded ChatMessage for testing return ChatMessage( ChatContext(null, null, false, false), "fake-message-for-group", @@ -205,17 +226,14 @@ class GnunetChatMock : GnunetChat { } override fun getMessageKind(message: ChatMessage): MessageKind { - // Always return TEXT for our mock return MessageKind.TEXT } override fun isMessageRecent(message: ChatMessage): GnunetReturnValue { - // Always return OK for our mock return GnunetReturnValue.OK } override fun getMessageTimestamp(message: ChatMessage): Long { - // Return a fixed, hard-coded timestamp for testing return 123456789L } @@ -231,7 +249,6 @@ class GnunetChatMock : GnunetChat { val contextAlice = ChatContext(ChatContextType.CONTACT, null, false, false) val contextBob = ChatContext(ChatContextType.CONTACT, null, false, false) val contacts = listOf( - ChatContact(contextAlice, name = "Alice"), ChatContact(contextBob, name = "Bob") ) @@ -252,36 +269,77 @@ class GnunetChatMock : GnunetChat { callback(group) } - messageCallback(ChatContext(ChatContextType.CONTACT, null, true, false), - ChatMessage(ChatContext(ChatContextType.CONTACT,null, true, false),"",0, - ChatContact(ChatContext(ChatContextType.CONTACT, null, true, false), "Mallory"),MessageKind.JOIN, null) + messageCallback( + ChatContext(ChatContextType.CONTACT, null, true, false), + ChatMessage( + ChatContext(ChatContextType.CONTACT, null, true, false), + "", + 0, + ChatContact(ChatContext(ChatContextType.CONTACT, null, true, false), "Mallory"), + MessageKind.JOIN, + null + ) ) - messageCallback(ChatContext(ChatContextType.CONTACT,"5", true, false), - ChatMessage(ChatContext(ChatContextType.CONTACT,null, true, false),"Hi, I am Mallory!",0, - ChatContact(ChatContext(ChatContextType.CONTACT, "6", true, false), "Mallory"),MessageKind.TEXT, null) + messageCallback( + ChatContext(ChatContextType.CONTACT, "5", true, false), + ChatMessage( + ChatContext(ChatContextType.CONTACT, null, true, false), + "Hi, I am Mallory!", + 0, + ChatContact(ChatContext(ChatContextType.CONTACT, "6", true, false), "Mallory"), + MessageKind.TEXT, + null + ) ) - messageCallback(ChatContext(ChatContextType.GROUP,"3", true, false), - ChatMessage(ChatContext(ChatContextType.GROUP,null, true, false),"",0, - ChatContact(ChatContext(ChatContextType.CONTACT, null, true, false), "Flo"),MessageKind.JOIN, null) + messageCallback( + ChatContext(ChatContextType.GROUP, "3", true, false), + ChatMessage( + ChatContext(ChatContextType.GROUP, null, true, false), + "", + 0, + ChatContact(ChatContext(ChatContextType.CONTACT, null, true, false), "Flo"), + MessageKind.JOIN, + null + ) ) - messageCallback(ChatContext(ChatContextType.GROUP,"3", true, false), - ChatMessage(ChatContext(ChatContextType.GROUP,null, true, false),"Hi, I am Flo!",0, - ChatContact(ChatContext(ChatContextType.CONTACT, "7", true, false), "Flo"),MessageKind.TEXT, null) + messageCallback( + ChatContext(ChatContextType.GROUP, "3", true, false), + ChatMessage( + ChatContext(ChatContextType.GROUP, null, true, false), + "Hi, I am Flo!", + 0, + ChatContact(ChatContext(ChatContextType.CONTACT, "7", true, false), "Flo"), + MessageKind.TEXT, + null + ) ) - messageCallback(ChatContext(ChatContextType.GROUP,"3", true, false), - ChatMessage(ChatContext(ChatContextType.GROUP,null, true, false),"",0, - ChatContact(ChatContext(ChatContextType.CONTACT, "1", true, false), "Alice"),MessageKind.JOIN, null) + messageCallback( + ChatContext(ChatContextType.GROUP, "3", true, false), + ChatMessage( + ChatContext(ChatContextType.GROUP, null, true, false), + "", + 0, + ChatContact(ChatContext(ChatContextType.CONTACT, "1", true, false), "Alice"), + MessageKind.JOIN, + null + ) ) - messageCallback(ChatContext(ChatContextType.GROUP,"3", true, false), - ChatMessage(ChatContext(ChatContextType.GROUP,null, true, false),"Hi, I am Alice!",0, - ChatContact(ChatContext(ChatContextType.CONTACT, "1", true, false), "Alice"),MessageKind.TEXT, null) + messageCallback( + ChatContext(ChatContextType.GROUP, "3", true, false), + ChatMessage( + ChatContext(ChatContextType.GROUP, null, true, false), + "Hi, I am Alice!", + 0, + ChatContact(ChatContext(ChatContextType.CONTACT, "1", true, false), "Alice"), + MessageKind.TEXT, + null + ) ) - } override fun getContactContext(chatContact: ChatContact): ChatContext { @@ -293,7 +351,6 @@ class GnunetChatMock : GnunetChat { } override fun getContactUserPointer(chatContact: ChatContact): String { - // Return a fixed, hard-coded string for testing return "fake-user-pointer-123" } @@ -302,7 +359,6 @@ class GnunetChatMock : GnunetChat { } override fun getGroupUserPointer(chatGroup: ChatGroup): String { - // Return a fixed, hard-coded string for testing return "fake-group-pointer-789" } @@ -320,7 +376,7 @@ class GnunetChatMock : GnunetChat { override fun getContextContact(context: ChatContext): ChatContact { println("get contact for context") - return ChatContact(context,"test") + return ChatContact(context, "test") } override fun deleteContact(chatContact: ChatContact) { @@ -341,7 +397,6 @@ class GnunetChatMock : GnunetChat { ) { val contextCharlie = ChatContext(ChatContextType.CONTACT, null, true, false) val contacts = listOf( - ChatContact(contextCharlie, name = "Charlie") ) for (contact in contacts) { @@ -377,12 +432,11 @@ class GnunetChatMock : GnunetChat { } override suspend fun reset() { - // SAFETY CHECK: Only allow reset in debug builds if (!org.gnunet.gnunetmessenger.BuildConfig.ALLOW_RESET) { println("reset: BLOCKED - Reset not allowed in production builds") throw SecurityException("Reset operation is not allowed in production") } - + println("reset mock service - clearing all state") uuidCounter = 0 lastDestroyedUri = null @@ -391,4 +445,4 @@ class GnunetChatMock : GnunetChat { lastSetMessageForGroupContact = null messageCallback = { _, _ -> } } -} +} +\ 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 @@ -39,6 +39,7 @@ import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import kotlinx.coroutines.Job import kotlinx.coroutines.launch import org.gnunet.gnunetmessenger.MainActivity import org.gnunet.gnunetmessenger.R @@ -54,6 +55,7 @@ class AccountListFragment : Fragment() { private lateinit var adapter: AccountAdapter private val accounts = mutableListOf<ChatAccount>() + private var refreshCollectorJob: Job? = null companion object { private const val TAG = "AccountListFragment" @@ -84,6 +86,7 @@ class AccountListFragment : Fragment() { Log.w(TAG, "disconnect before connect failed", it) } } + gnunetChat.connect(handle, selectedAccount) selectedAccount.key = gnunetChat.getProfileKey(handle) activity.setCurrentAccount(selectedAccount) @@ -118,29 +121,21 @@ class AccountListFragment : Fragment() { super.onViewCreated(view, savedInstanceState) val activity = requireActivity() as MainActivity - val gnunetChat = activity.getGnunetChatInstance() viewLifecycleOwner.lifecycleScope.launch { try { - val handle = activity.awaitChatReady() - Log.d(TAG, "Chat ready, loading accounts for handle=${handle.pointer}") + val handle = activity.awaitInitialDataReady() + Log.d(TAG, "Initial refresh received, loading accounts for handle=${handle.pointer}") - showLoading(getString(R.string.loading_accounts)) createButton.isEnabled = true - recycler.isVisible = true - loadingIndicator.isGone = true - - gnunetChat.iterateAccounts(handle) { account -> - account.key = gnunetChat.getProfileKey(handle) - accounts.add(account) - adapter.submitList(accounts.toList()) - statusText.isGone = true - recycler.isVisible = true - } + reloadAccounts(showLoading = true) - if (accounts.isEmpty()) { - statusText.text = getString(R.string.no_accounts_available) - statusText.isVisible = true + refreshCollectorJob?.cancel() + refreshCollectorJob = viewLifecycleOwner.lifecycleScope.launch { + activity.accountRefreshFlow().collect { + Log.d(TAG, "Account refresh event received") + reloadAccounts(showLoading = false) + } } } catch (t: Throwable) { Log.e(TAG, "Failed to initialize account list", t) @@ -149,6 +144,49 @@ class AccountListFragment : Fragment() { } } + override fun onDestroyView() { + refreshCollectorJob?.cancel() + refreshCollectorJob = null + super.onDestroyView() + } + + private suspend fun reloadAccounts(showLoading: Boolean) { + val activity = requireActivity() as MainActivity + val gnunetChat = activity.getGnunetChatInstance() + val handle = activity.getChatHandle() + + if (showLoading) { + showLoading(getString(R.string.loading_accounts)) + } + + try { + Log.d(TAG, "listAccounts(): handle=${handle.pointer}") + val refreshedAccounts = gnunetChat.listAccounts(handle).map { account -> + account.key = gnunetChat.getProfileKey(handle) + account + } + + accounts.clear() + accounts.addAll(refreshedAccounts) + adapter.submitList(accounts.toList()) + + loadingIndicator.isGone = true + createButton.isEnabled = true + + if (accounts.isEmpty()) { + recycler.isGone = true + statusText.isVisible = true + statusText.text = getString(R.string.no_accounts_available) + } else { + statusText.isGone = true + recycler.isVisible = true + } + } catch (t: Throwable) { + Log.e(TAG, "reloadAccounts failed", t) + showError(getString(R.string.account_list_load_failed)) + } + } + private fun showLoading(message: String) { loadingIndicator.isVisible = true statusText.isVisible = true diff --git a/GNUnetMessenger/app/src/main/res/values/strings.xml b/GNUnetMessenger/app/src/main/res/values/strings.xml @@ -30,9 +30,11 @@ <string name="connecting_to_gnunet">Connecting to GNUnet…</string> <string name="loading_accounts">Loading accounts…</string> + <string name="waiting_for_initial_refresh">Waiting for initial refresh…</string> <string name="no_accounts_available">No accounts available yet.</string> <string name="gnunet_connection_failed">Could not connect to GNUnet.</string> <string name="account_connect_failed">Could not activate the selected account.</string> + <string name="account_list_load_failed">Could not load the account list.</string> <string-array name="lobby_lifetimes"> <item>Off</item>