commit 7803c39f8c0990884ce91f66e9e52fbe85e1aad4 parent 3824d52fd963bebfa85f4e0334622d264840a04a Author: t3sserakt <t3sserakt@posteo.de> Date: Wed, 26 Nov 2025 19:58:52 +0100 First attempt to call remote intefaces. Diffstat:
16 files changed, 751 insertions(+), 137 deletions(-)
diff --git a/.gitignore b/.gitignore @@ -1,2 +1,3 @@ *~ .DS_Store +GNUnetMessenger/app/release/ diff --git a/GNUnetMessenger/app/build.gradle.kts b/GNUnetMessenger/app/build.gradle.kts @@ -39,6 +39,7 @@ android { buildFeatures { dataBinding = true viewBinding = true + aidl = true } } @@ -62,5 +63,6 @@ dependencies { testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) - + implementation("org.gnunet:gnunet-ipc-contract:1.0.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1") } \ No newline at end of file diff --git a/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/MainActivity.kt b/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/MainActivity.kt @@ -69,7 +69,7 @@ class MainActivity : AppCompatActivity() { setContentView(R.layout.activity_main) // Initialize GnunetChat (keep this line!) - gnunetChat = ServiceFactory.create(this, useMock = true) + gnunetChat = ServiceFactory.create(this, useMock = false) val app = MessengerApp() handle = gnunetChat.startChat(app) { chatContext, chatMessage -> diff --git a/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/ipc/DtoMappers.kt b/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/ipc/DtoMappers.kt @@ -0,0 +1,43 @@ +package org.gnunet.gnunetmessenger.ipc // oder dein passendes Paket + + +import org.gnunet.gnunetmessenger.model.ChatAccount +import org.gnunet.gnunetmessenger.model.ChatContext +import org.gnunet.gnunetmessenger.model.ChatMessage +import org.gnunet.gnunetmessenger.model.ChatContextType +import org.gnunet.gnunetmessenger.model.MessageKind +import org.gnunet.gnunetmessenger.model.ChatMessageType + +fun ChatAccountDto.toLocal(): ChatAccount { + return ChatAccount( + key = this.key ?: "", + name = this.name ?: "", + pointer = 0 + ) +} + +fun ChatContextDto.toLocal(): ChatContext { + val type = ChatContextType.fromCode(chatContextType) + return ChatContext( + chatContextType = type, + userPointer = userPointer?.takeIf { it.isNotEmpty() }, + isGroup = isGroup, + isPlatform = isPlatform + ) +} + +fun ChatMessageDto.toLocal(ctx: ChatContext): ChatMessage { + val kindEnum = MessageKind.fromCode(kind) + + val typeEnum = if (type < 0) null + else ChatMessageType.fromCode(type) + + return ChatMessage( + chatContext = ctx, + text = text ?: "", + timestamp = timestamp, + sender = null, // senderKey kannst du später über ein Repo auflösen + kind = kindEnum, + type = typeEnum + ) +} +\ No newline at end of file diff --git a/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/model/ChatContextType.kt b/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/model/ChatContextType.kt @@ -24,6 +24,11 @@ package org.gnunet.gnunetmessenger.model -enum class ChatContextType { - CONTACT, GROUP, UNKNOWN +enum class ChatContextType(val code: Int) { + CONTACT(0), GROUP(1), UNKNOWN(2); + + companion object { + fun fromCode(code: Int): ChatContextType = + entries.firstOrNull { it.code == code } ?: CONTACT + } } \ No newline at end of file diff --git a/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/model/ChatHandle.kt b/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/model/ChatHandle.kt @@ -24,6 +24,4 @@ package org.gnunet.gnunetmessenger.model -class ChatHandle(pointer: Long) { - val pointer: Long = pointer -} +class ChatHandle(@Volatile var pointer: Long) diff --git a/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/model/ChatMessageType.kt b/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/model/ChatMessageType.kt @@ -24,8 +24,13 @@ package org.gnunet.gnunetmessenger.model -enum class ChatMessageType { - OWN, - OTHER, - SYSTEM +enum class ChatMessageType(val code: Int) { + OWN(0), + OTHER(1), + SYSTEM(2); + + companion object { + fun fromCode(code: Int): ChatMessageType = + entries.firstOrNull { it.code == code } ?: OWN + } } 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 @@ -24,12 +24,17 @@ package org.gnunet.gnunetmessenger.model -enum class MessageKind { - WARNING, REFRESH, LOGIN, LOGOUT, - CREATED_ACCOUNT, UPDATE_ACCOUNT, - UPDATE_CONTEXT, JOIN, LEAVE, - CONTACT, SHARED_ATTRIBUTES, - INVITATION, TEXT, FILE, - DELETION, TAG, ATTRIBUTES, - DISCOURSE, DATA +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); + + companion object { + fun fromCode(code: Int): MessageKind = + entries.firstOrNull { it.code == code } ?: WARNING + } } 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 @@ -37,13 +37,14 @@ import org.gnunet.gnunetmessenger.model.MessageKind import org.gnunet.gnunetmessenger.model.MessengerApp interface GnunetChat { + suspend fun awaitReady(handle: ChatHandle) fun startChat(messengerApp: MessengerApp, callback: (ChatContext, ChatMessage) -> Unit): ChatHandle fun iterateAccounts(handle: ChatHandle, callback: (ChatAccount) -> Unit) - fun createAccount(handle: ChatHandle, name: String): GnunetReturnValue - fun connect(handle: ChatHandle, account: ChatAccount) - fun disconnect(handle: ChatHandle) - fun getProfileName(handle: ChatHandle): String - fun setProfileName(handle: ChatHandle, name: String) + 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 diff --git a/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/service/ServiceFactory.kt b/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/service/ServiceFactory.kt @@ -30,7 +30,12 @@ import org.gnunet.gnunetmessenger.service.mock.GnunetChatMock object ServiceFactory { - fun create(context: Context, useMock: Boolean): GnunetChat { - return if (useMock) GnunetChatMock() else GnunetChatBoundService() + fun create(context: android.content.Context, useMock: Boolean): GnunetChat { + return if (useMock) { + GnunetChatMock() + } else { + // Delegator, der per AIDL auf den Remote-Service ruft + GnunetChatBoundService(context.applicationContext) + } } } \ 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 @@ -1,112 +1,536 @@ -/* - 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/boundimpl/GnunetChatBoundService.kt - */ - package org.gnunet.gnunetmessenger.service.boundimpl - +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.RemoteException +import android.util.Log +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout +import org.gnunet.gnunetmessenger.ipc.ChatAccountDto +import org.gnunet.gnunetmessenger.ipc.ChatContextDto +import org.gnunet.gnunetmessenger.ipc.ChatMessageDto +import org.gnunet.gnunetmessenger.ipc.IAccountCallback +import org.gnunet.gnunetmessenger.ipc.IChatCallback +import org.gnunet.gnunetmessenger.ipc.IGnunetChat import org.gnunet.gnunetmessenger.model.ChatAccount import org.gnunet.gnunetmessenger.model.ChatContact import org.gnunet.gnunetmessenger.model.ChatContext +import org.gnunet.gnunetmessenger.model.ChatContextType import org.gnunet.gnunetmessenger.model.ChatGroup import org.gnunet.gnunetmessenger.model.ChatHandle import org.gnunet.gnunetmessenger.model.ChatMessage +import org.gnunet.gnunetmessenger.model.ChatMessageType import org.gnunet.gnunetmessenger.model.ChatUri import org.gnunet.gnunetmessenger.model.GnunetReturnValue import org.gnunet.gnunetmessenger.model.MessageKind import org.gnunet.gnunetmessenger.model.MessengerApp import org.gnunet.gnunetmessenger.service.GnunetChat -import java.util.UUID +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicReference + +class GnunetChatBoundService( + private val appContext: Context +) : GnunetChat { + + 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 pendingStart: PendingStart? = null + + 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) } + } 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) + lastHandle = 0L + deathRecipient = null + } + 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 != null && 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) { + Log.w(TAG, "Remote disconnected") + remoteRef.set(null) + lastHandle = 0L + } + } + + init { + // proaktiv binden (damit es später schneller geht) + bind() + } + + private suspend fun getOrBindRemote(maxWaitMs: Long = 2_000): IGnunetChat { + remoteRef.get()?.let { return it } + bind() + var waited = 0L + while (waited < maxWaitMs) { + remoteRef.get()?.let { return it } + delay(100) + waited += 100 + } + throw IllegalStateException("Remote not connected (timeout)") + } + + private suspend inline fun <T> withReadyRemote( + handle: ChatHandle, + 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 + } + if (remote != null) { + try { + return block(remote, handle.pointer) + } catch (dead: android.os.DeadObjectException) { + // Binder tot -> rebind und retry + 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++ + } + throw RuntimeException("Remote call failed after retries", lastError) + } + + private fun bind(): Boolean { + val intent = Intent(ACTION_BIND_GNUNET_CHAT).setPackage(SERVER_PACKAGE) + return appContext.bindService(intent, conn, Context.BIND_AUTO_CREATE) + } + + fun unbind() { + val remote = remoteRef.get() + val dr = deathRecipient + if (remote != null && dr != null) { + runCatching { remote.asBinder().unlinkToDeath(dr, 0) } + .onFailure { Log.w(TAG, "unlinkToDeath failed", it) } + } + deathRecipient = null + remoteRef.set(null) + lastHandle = 0L + runCatching { appContext.unbindService(conn) } + .onFailure { Log.w(TAG, "unbindService failed", it) } + } + + 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 + if (handle.pointer == 0L) { + handle.pointer = h // zur Sicherheit setzen, falls Racing + } + 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 + } + } + + // 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) + } + } + + // 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") + } -class GnunetChatBoundService : GnunetChat { override fun startChat( messengerApp: MessengerApp, callback: (ChatContext, ChatMessage) -> Unit ): ChatHandle { - TODO("Not yet implemented") + messageCallback = callback + + // Platzhalter-Handle sofort zurückgeben (UI blockiert nicht) + val ch = ChatHandle(0L) + + val remote = remoteRef.get() + if (remote != null && lastHandle == 0L) { + // Wir sind gebunden, aber noch kein Server-Handle -> jetzt starten + 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 if (remote == null) { + // 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}") + } + } + } else if (remote != null && lastHandle != 0L) { + // Bereits eine Session vorhanden (Single-Session-Design) -> denselben Handle verwenden + ch.pointer = lastHandle + } + + return ch } override fun iterateAccounts(handle: ChatHandle, callback: (ChatAccount) -> Unit) { - TODO("Not yet implemented") - } - - override fun createAccount(handle: ChatHandle, name: String): GnunetReturnValue { - TODO("Not yet implemented") + val bridge = object : IAccountCallback.Stub() { + override fun onAccount(accountDto: ChatAccountDto) { + val acc = accountDto.toLocal() + mainScope.launch { callback(acc) } + } + override fun onDone() { Log.d(TAG, "iterateAccounts: done") } + override fun onError(code: Int, message: String?) { + Log.e(TAG, "iterateAccounts: error $code $message") + } + } + + val remote = remoteRef.get() + val h = lastHandle.takeIf { it != 0L } ?: handle.pointer + + when { + remote != null && h != 0L -> { + ioScope.launch { + try { + remote.iterateAccounts(h, bridge) + } catch (dead: android.os.DeadObjectException) { + // Binder tot -> re-queue und rebind + 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) } + } + } + 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) } + } + } + bind() + } + } + } + + // Verbunden, aber Handle noch nicht bereit → aufschieben bis handle da ist + remote != null && h == 0L -> { + synchronized(pendingAfterHandle) { + pendingAfterHandle += { r, real -> + runCatching { r.iterateAccounts(real, bridge) } + .onFailure { Log.e(TAG, "iterateAccounts (deferred) failed", it) } + } + } + } + + // Noch nicht verbunden → binden & aufschieben + else -> { + synchronized(pendingAfterHandle) { + pendingAfterHandle += { r, real -> + runCatching { r.iterateAccounts(real, bridge) } + .onFailure { Log.e(TAG, "iterateAccounts (deferred) failed", it) } + } + } + bind() + } + } + } + + // --- Helpers --- + + private fun drainPending(remote: IGnunetChat, handle: Long) { + val tasks = synchronized(pendingAfterHandle) { + if (pendingAfterHandle.isEmpty()) return + val copy = pendingAfterHandle.toList() + pendingAfterHandle.clear() + copy + } + ioScope.launch { + tasks.forEach { task -> + runCatching { task(remote, handle) } + .onFailure { Log.e(TAG, "deferred task failed", it) } + } + } + } + + 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) } + } + return when (code) { + 0 -> GnunetReturnValue.OK + else -> GnunetReturnValue.NO + } + } + + override suspend fun connect(handle: ChatHandle, account: ChatAccount) { + withReadyRemote(handle) { remote, h -> + withContext(Dispatchers.IO) { remote.connect(h, account.toDto()) } + } + } + + override suspend fun disconnect(handle: ChatHandle) { + withReadyRemote(handle) { remote, h -> + withContext(Dispatchers.IO) { remote.disconnect(h) } + } + } + + override suspend fun getProfileName(handle: ChatHandle): String { + return withReadyRemote(handle) { remote, h -> + withContext(Dispatchers.IO) { remote.getProfileName(h) ?: "" } + } + } + + override suspend fun setProfileName(handle: ChatHandle, name: String) { + withReadyRemote(handle) { remote, h -> + withContext(Dispatchers.IO) { remote.setProfileName(h, name) } + } + } + + + companion object { + private const val TAG = "GnunetChatBoundService" + private const val ACTION_BIND_GNUNET_CHAT = + "org.gnunet.gnunetmessenger.ipc.BIND_GNUNET_CHAT" + private const val SERVER_PACKAGE = "org.gnu.gnunet" + private const val DEFAULT_APP_NAME = "Default" + } + + // ---------------- DTO -> App Mapper ---------------- + + private fun ChatAccount.toDto(): ChatAccountDto = + ChatAccountDto().apply { + key = this@toDto.key + name = this@toDto.name + } + + private fun ChatAccountDto.toLocal(): ChatAccount { + return ChatAccount( + key = this.key ?: "", + name = this.name ?: "", + pointer = 0 // falls du hier etwas Eigenes brauchst + ) } - override fun connect(handle: ChatHandle, account: ChatAccount) { - TODO("Not yet implemented") - } - - override fun disconnect(handle: ChatHandle) { - TODO("Not yet implemented") - } + private fun ChatContextDto.toLocal(): ChatContext { + val type = ChatContextType.fromCode(chatContextType) + return ChatContext( + chatContextType = type, + userPointer = userPointer?.takeIf { it.isNotEmpty() }, + isGroup = isGroup, + isPlatform = isPlatform + ) + } - override fun getProfileName(handle: ChatHandle): String { - TODO("Not yet implemented") + private fun ChatMessageDto.toLocal(ctx: ChatContext): ChatMessage { + val kindEnum = MessageKind.fromCode(kind) + val typeEnum = if (type < 0) null else ChatMessageType.fromCode(type) + return ChatMessage( + chatContext = ctx, + text = text ?: "", + timestamp = timestamp, + sender = null, // senderKey kannst du später auflösen + kind = kindEnum, + type = typeEnum + ) } - override fun setProfileName(handle: ChatHandle, name: String) { - TODO("Not yet implemented") - } override fun getProfileKey(handle: ChatHandle): String { - TODO("Not yet implemented") + return "somekey1337" } - override fun setContactBlocked(contact: ChatContact, isBlocked: Boolean) { - TODO("Not yet implemented") + override fun isContactBlocked(contact: ChatContact): Boolean { + return true } - override fun isContactBlocked(contact: ChatContact): Boolean { - TODO("Not yet implemented") + override fun setContactBlocked(contact: ChatContact, isBlocked: Boolean) { + println("isblocked:" + isBlocked) } override fun setAttribute(handle: ChatHandle, key: String, value: String) { - TODO("Not yet implemented") + println("setting Attribute withj key: ${key} and value ${value}") } override fun getAttributes(handle: ChatHandle, callback: (String, String) -> Unit) { - TODO("Not yet implemented") + val mockAttributes = listOf( + "nickname" to "Alice", + "location" to "Berlin", + "status" to "Online" + ) + + for ((key, value) in mockAttributes) { + callback(key, value) + } } override fun lobbyOpen(handle: ChatHandle, callback: (String) -> Unit) { - TODO("Not yet implemented") + callback("000G006K2TJNMD9VTCYRX7BRVV3HAEPS15E6NHDXKPJA1KAJJEG9AFF884") } override fun lobbyJoin( handle: ChatHandle, uri: String ) { - TODO("Not yet implemented") + println("join lobby") } override fun setGroupName(group: ChatGroup, name: String) { - TODO("Not yet implemented") + group.name = name } override fun createGroup(handle: ChatHandle, topic: String): ChatGroup { - TODO("Not yet implemented") + return ChatGroup(ChatContext(ChatContextType.GROUP, null, + true, false), topic) } override fun parseUri(uri: String): ChatUri { @@ -118,23 +542,28 @@ class GnunetChatBoundService : GnunetChat { } override fun inviteContactToGroup(group: ChatGroup, contact: ChatContact) { - TODO("Not yet implemented") + println("contact ${contact.name} invited") } - override fun getUserPointerForContext(context: ChatContext): String { - TODO("Not yet implemented") + override fun getUserPointerForContext(context: ChatContext): String? { + return context.userPointer } override fun setUserPointerForContext(context: ChatContext, userPointer: String) { - TODO("Not yet implemented") + context.userPointer = userPointer } override fun getSenderFromMessage(message: ChatMessage): ChatContact { - TODO("Not yet implemented") + return ChatContact(ChatContext(ChatContextType.CONTACT, null, true, false), message.sender?.name ?: "") } - override fun getGroupFromContext(context: ChatContext): ChatGroup { - TODO("Not yet implemented") + override fun getGroupFromContext(context: ChatContext): ChatGroup? { + if ("3" == context.userPointer){ + val contextDev = ChatContext(ChatContextType.GROUP, null, true, false) + return ChatGroup(contextDev, name = "Dev Team") + } + val contextDev = ChatContext(ChatContextType.GROUP, null, true, false) + return ChatGroup(contextDev, name = "GNUnet Team") } override fun getMessageForGroupContact(group: ChatGroup, contact: ChatContact): ChatMessage { @@ -162,19 +591,68 @@ class GnunetChatBoundService : GnunetChat { } override fun iterateContacts(handle: ChatHandle, callback: (ChatContact) -> Int) { - TODO("Not yet implemented") + 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") + ) + for (contact in contacts) { + callback(contact) + } } override fun iterateGroups(handle: ChatHandle, callback: (ChatGroup) -> Int) { - TODO("Not yet implemented") + println("iterate groups") + val contextDev = ChatContext(ChatContextType.GROUP, null, true, false) + val contextFriends = ChatContext(ChatContextType.GROUP, null, true, false) + val groups = listOf( + ChatGroup(contextDev, name = "Dev Team"), + ChatGroup(contextFriends, name = "Friends") + ) + for (group in groups) { + 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,"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),"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),"Hi, I am Alice!",0, + ChatContact(ChatContext(ChatContextType.CONTACT, "1", true, false), "Alice"),MessageKind.TEXT, null) + ) + } override fun getContactContext(chatContact: ChatContact): ChatContext { - TODO("Not yet implemented") + return chatContact.chatContext } override fun getGroupContext(chatGroup: ChatGroup): ChatContext { - TODO("Not yet implemented") + return chatGroup.chatContext } override fun getContactUserPointer(chatContact: ChatContact): String { @@ -190,56 +668,73 @@ class GnunetChatBoundService : GnunetChat { } override fun setGroupUserPointer(chatGroup: ChatGroup, userPointer: String) { - TODO("Not yet implemented") + println("set group name") } override fun sendText(chatContext: ChatContext, text: String) { - TODO("Not yet implemented") + println("send text: $text") } override fun getContactKey(chatContact: ChatContact): String { - TODO("Not yet implemented") + return "otherkey42" } override fun getContextContact(context: ChatContext): ChatContact { - TODO("Not yet implemented") + println("get contact for context") + return ChatContact(context,"test") } override fun deleteContact(chatContact: ChatContact) { - TODO("Not yet implemented") + println("delete contact") } override fun isGroup(context: ChatContext): Boolean { - TODO("Not yet implemented") + return context.isGroup } override fun isPlatform(context: ChatContext): Boolean { - TODO("Not yet implemented") + return context.isPlatform } override fun iterateGroupContacts( chatGroup: ChatGroup, callback: (ChatGroup, ChatContact) -> Int ) { - TODO("Not yet implemented") + val contextCharlie = ChatContext(ChatContextType.CONTACT, null, true, false) + val contacts = listOf( + + ChatContact(contextCharlie, name = "Charlie") + ) + for (contact in contacts) { + callback(chatGroup, contact) + } } override fun randomUUID(): String { - return UUID.randomUUID().toString() + return uuidCounter++.toString() } override fun getContactAttributes( contact: ChatContact, callback: (String, String) -> Unit ) { - TODO("Not yet implemented") + val mockAttributes = listOf( + "nickname" to "Alice", + "location" to "Berlin", + "status" to "Online" + ) + + for ((key, value) in mockAttributes) { + callback(key, value) + } } override fun shareAttributes(handle: ChatHandle, contact: ChatContact, key: String) { - TODO("Not yet implemented") + println("share ${key} for contact ${contact.name}") } override fun unshareAttributes(handle: ChatHandle, contact: ChatContact, key: String) { - TODO("Not yet implemented") + println("unshare ${key} for contact ${contact.name}") } + } \ 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 @@ -44,6 +44,10 @@ class GnunetChatMock : GnunetChat { private lateinit var messageCallback: (ChatContext, ChatMessage) -> Unit private var uuidCounter: Long = 0 + override suspend fun awaitReady(handle: ChatHandle) { + return + } + override fun startChat( messengerApp: MessengerApp, callback: (ChatContext, ChatMessage) -> Unit @@ -65,19 +69,19 @@ class GnunetChatMock : GnunetChat { } } - override fun createAccount(handle: ChatHandle, name: String): GnunetReturnValue { + override suspend fun createAccount(handle: ChatHandle, name: String): GnunetReturnValue { println("create account") return GnunetReturnValue.OK } - override fun connect(handle: ChatHandle, account: ChatAccount) { + 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) ) } - override fun disconnect(handle: ChatHandle) { + override suspend fun disconnect(handle: ChatHandle) { println("disconnect") uuidCounter = 0 messageCallback(ChatContext(ChatContextType.UNKNOWN,UUID.randomUUID().toString(), false, false), @@ -85,11 +89,11 @@ class GnunetChatMock : GnunetChat { ) } - override fun getProfileName(handle: ChatHandle): String { + override suspend fun getProfileName(handle: ChatHandle): String { return "somename" + (1000..9999).random() } - override fun setProfileName(handle: ChatHandle, name: String) { + override suspend fun setProfileName(handle: ChatHandle, name: String) { println("set profile name") } diff --git a/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/ui/account/AccountDetailsFragment.kt b/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/ui/account/AccountDetailsFragment.kt @@ -29,7 +29,9 @@ import android.graphics.BitmapFactory import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController +import kotlinx.coroutines.launch import org.gnunet.gnunetmessenger.MainActivity import org.gnunet.gnunetmessenger.R import org.gnunet.gnunetmessenger.ui.BaseProfileFragment @@ -49,15 +51,28 @@ class AccountDetailsFragment : BaseProfileFragment() { val gnunetChat = activity.getGnunetChatInstance() val handle = activity.getChatHandle() - val profileName = gnunetChat.getProfileName(handle) + + viewLifecycleOwner.lifecycleScope.launch { + try { + nameEdit.setText(gnunetChat.getProfileName(handle)) + } catch (t: Throwable) { + // optional: Fehlermeldung / Toast + } + } nameText.visibility = View.GONE nameEdit.visibility = View.VISIBLE saveNameButton.visibility = View.VISIBLE - nameEdit.setText(profileName) + saveNameButton.setOnClickListener { val newName = nameEdit.text.toString() - gnunetChat.setProfileName(handle, newName) + viewLifecycleOwner.lifecycleScope.launch { + try { + gnunetChat.setProfileName(handle, newName) + } catch (t: Throwable) { + // optional: Fehlermeldung / Toast + } + } } val avatar = AvatarStorageUtil.loadAvatar(requireContext(), gnunetChat.getProfileKey(handle)) 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 @@ -25,14 +25,17 @@ package org.gnunet.gnunetmessenger.ui.account import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Button import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import kotlinx.coroutines.launch import org.gnunet.gnunetmessenger.MainActivity import org.gnunet.gnunetmessenger.R import org.gnunet.gnunetmessenger.model.ChatAccount @@ -44,6 +47,10 @@ class AccountListFragment : Fragment() { private lateinit var createButton: Button private lateinit var adapter: AccountAdapter + companion object { + private const val TAG = "AccountListFragment" + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? @@ -58,9 +65,18 @@ class AccountListFragment : Fragment() { createButton = view.findViewById(R.id.btn_create_account) adapter = AccountAdapter { selectedAccount -> - if (null != activity.currentAccount) - gnunetChat.disconnect(handle) - gnunetChat.connect(handle, selectedAccount) + + viewLifecycleOwner.lifecycleScope.launch { + try { + if (null != activity.currentAccount) + runCatching { gnunetChat.disconnect(handle) } + .onFailure { /* loggen/toast, aber trotzdem weiter */ } + gnunetChat.connect(handle, selectedAccount) + } catch (t: Throwable) { + // optional: Fehlermeldung / Toast + } + } + selectedAccount.key = gnunetChat.getProfileKey(handle) val action = AccountListFragmentDirections.actionAccountListFragmentToAccountOverviewFragment(account = selectedAccount) findNavController().navigate(action) @@ -73,7 +89,7 @@ class AccountListFragment : Fragment() { val action = AccountListFragmentDirections.actionAccountListFragmentToCreateAccountFragment() findNavController().navigate(action) } - + Log.d(TAG, "iterateAccounts(): handle=${handle.pointer}") (activity as MainActivity).getGnunetChatInstance().iterateAccounts((activity as MainActivity).getChatHandle()) { account -> account.key = gnunetChat.getProfileKey(handle) list.add(account) diff --git a/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/ui/account/CreateAccountFragment.kt b/GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/ui/account/CreateAccountFragment.kt @@ -31,7 +31,9 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.activity.result.contract.ActivityResultContracts +import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController +import kotlinx.coroutines.launch import org.gnunet.gnunetmessenger.MainActivity import org.gnunet.gnunetmessenger.databinding.FragmentCreateAccountBinding import org.gnunet.gnunetmessenger.model.ChatHandle @@ -92,28 +94,43 @@ class CreateAccountFragment : Fragment() { } private fun createAccount(accountName: String) { - val result = gnunetChat.createAccount(handle = ChatHandle(1), name = accountName) - - when (result) { - GnunetReturnValue.OK -> { - gnunetChat.iterateAccounts(handle) { account -> - if (account.name == accountName) { - if (null != mainActivity.currentAccount) - gnunetChat.disconnect(handle) - gnunetChat.connect(handle, account) - requireActivity().runOnUiThread { - val action = - CreateAccountFragmentDirections.actionCreateAccountFragmentToAccountOverviewFragment( - account = account - ) - findNavController().navigate(action) - } - } + + viewLifecycleOwner.lifecycleScope.launch { + try { + val result = gnunetChat.createAccount(handle = ChatHandle(1), name = accountName) + + when (result) { + GnunetReturnValue.OK -> { + gnunetChat.iterateAccounts(handle) { account -> + if (account.name == accountName) { + viewLifecycleOwner.lifecycleScope.launch { + try { + if (null != mainActivity.currentAccount) + runCatching { gnunetChat.disconnect(handle) } + .onFailure { /* loggen/toast, aber trotzdem weiter */ } + gnunetChat.connect(handle, account) + } catch (t: Throwable) { + // optional: Fehlermeldung / Toast + } + } + requireActivity().runOnUiThread { + val action = + CreateAccountFragmentDirections.actionCreateAccountFragmentToAccountOverviewFragment( + account = account + ) + findNavController().navigate(action) + } + } + + } + } + else -> { + println("result ${result} not ok") + } } - } - else -> { - println("result ${result} not ok") + } catch (t: Throwable) { + // TODO: Fehlermeldung / Toast / Log } } } diff --git a/GNUnetMessenger/settings.gradle.kts b/GNUnetMessenger/settings.gradle.kts @@ -17,6 +17,7 @@ pluginManagement { dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { + mavenLocal() google() mavenCentral() }