gnunet-android

GNUnet for Android
Log | Files | Refs | README

GnunetChatIpcBridge.cpp (21619B)


      1 #include <jni.h>
      2 #include <map>
      3 #include <mutex>
      4 #include <memory>
      5 #include <atomic>
      6 #include <string>
      7 #include <android/log.h>
      8 #include <android/asset_manager.h>
      9 #include <android/asset_manager_jni.h>
     10 #include "gnunet_chat_lib.h"
     11 #include "gnunet_util_lib.h"
     12 
     13 
     14 #define LOG_TAG "GnunetChatIpcBridge"
     15 #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
     16 #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
     17 
     18 // ======================================================
     19 // Globale Verwaltung
     20 // ======================================================
     21 
     22 static JavaVM* g_vm = nullptr;
     23 
     24 // Session-Map: handle (jlong) -> ChatSession
     25 static std::mutex g_mapMtx;
     26 struct ChatSession;
     27 static std::map<long, std::unique_ptr<ChatSession>> g_sessions;
     28 static std::atomic_long g_nextHandle{1};
     29 
     30 static const struct GNUNET_CONFIGURATION_Handle* g_cfg = nullptr;
     31 static std::atomic_bool g_cfgInited{false};
     32 
     33 // Hilfsfunktion: Thread-sicheres JNIEnv-Beschaffen + optionales Detach
     34 struct ScopedEnv {
     35     JNIEnv* env = nullptr;
     36     bool didAttach = false;
     37 
     38     ScopedEnv() {
     39         if (!g_vm) return;
     40         if (g_vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK || !env) {
     41             if (g_vm->AttachCurrentThread(&env, nullptr) == JNI_OK) {
     42                 didAttach = true;
     43             } else {
     44                 env = nullptr;
     45             }
     46         }
     47     }
     48     ~ScopedEnv() {
     49         if (didAttach && g_vm) {
     50             g_vm->DetachCurrentThread();
     51         }
     52     }
     53     JNIEnv* get() const { return env; }
     54 };
     55 
     56 // ======================================================
     57 // Session-Struktur mit GNUnet-Handle
     58 // ======================================================
     59 
     60 struct ChatSession {
     61     jlong handle = 0;
     62     // Optional: Java-Callback-Objekt (IChatCallback Implementierung aus deinem Service)
     63     jobject cbGlobal = nullptr;
     64 
     65     // Beispielhafte Session-Daten
     66     std::string profileName = "GNUnet";
     67     bool connected = false;
     68 
     69     // GNUnet Chat Handle
     70     struct GNUNET_CHAT_Handle* chatHandle = nullptr;
     71 
     72     ChatSession() = default;
     73 
     74     ~ChatSession() {
     75         // GlobalRef bereinigen
     76         if (cbGlobal) {
     77             ScopedEnv senv;
     78             if (auto* env = senv.get()) {
     79                 env->DeleteGlobalRef(cbGlobal);
     80             }
     81             cbGlobal = nullptr;
     82         }
     83 
     84         // GNUnet Chat stoppen
     85         if (chatHandle) {
     86             LOGD("Stopping GNUNET_CHAT handle for session %ld", (long)handle);
     87             GNUNET_CHAT_disconnect(chatHandle);
     88             chatHandle = nullptr;
     89         }
     90     }
     91 };
     92 
     93 // Helper: Session lookup
     94 static ChatSession* findSessionOrNull(long h) {
     95     std::lock_guard<std::mutex> lk(g_mapMtx);
     96     auto it = g_sessions.find(h);
     97     return (it == g_sessions.end()) ? nullptr : it->second.get();
     98 }
     99 
    100 // ======================================================
    101 // JNI Setup
    102 // ======================================================
    103 
    104 extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
    105     g_vm = vm;
    106     return JNI_VERSION_1_6;
    107 }
    108 
    109 // ======================================================
    110 // GNUnet Callback (eingehende Nachrichten)
    111 // -> Beispiel: ruft eine Methode am Java-Callback auf (onMessageReceived(String))
    112 // ======================================================
    113 
    114 static JNIEnv* GetEnv() {
    115     JNIEnv* env = nullptr;
    116     if (g_vm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) {
    117         if (g_vm->AttachCurrentThread(&env, nullptr) != JNI_OK) return nullptr;
    118     }
    119     return env;
    120 }
    121 
    122 // ---- kleine Helfer ----
    123 static void fillChatContextDto(JNIEnv* env, jobject jCtx, struct GNUNET_CHAT_Context* ctx) {
    124     if (!env || !jCtx) return;
    125     jclass cls = env->GetObjectClass(jCtx);
    126     if (!cls) return;
    127 
    128     jfieldID fType    = env->GetFieldID(cls, "chatContextType", "I");
    129     jfieldID fIsGroup = env->GetFieldID(cls, "isGroup", "Z");
    130     jfieldID fIsPlat  = env->GetFieldID(cls, "isPlatform", "Z");
    131     jfieldID fUserPtr = env->GetFieldID(cls, "userPointer", "Ljava/lang/String;");
    132 
    133     int type = 0;
    134     bool isGroup = false;
    135     bool isPlatform = false;
    136     // Heuristik: Ist eine Gruppe am Context?
    137     if (ctx) {
    138         if (GNUNET_CHAT_context_get_group(ctx)) { isGroup = true; type = 1; }
    139         // platform: falls du später einen Indikator hast → hier setzen
    140     }
    141 
    142     env->SetIntField(jCtx, fType, (jint) type);
    143     env->SetBooleanField(jCtx, fIsGroup, (jboolean) isGroup);
    144     env->SetBooleanField(jCtx, fIsPlat, (jboolean) isPlatform);
    145     env->SetObjectField(jCtx, fUserPtr, nullptr);
    146 }
    147 
    148 static jlong getTimestampMillis(struct GNUNET_CHAT_Message* msg) {
    149     if (!msg) return 0;
    150     time_t ts = GNUNET_CHAT_message_get_timestamp(msg);
    151     if (ts <= 0) return 0;
    152     return (jlong) ts * 1000LL;
    153 }
    154 
    155 // ---- Haupt-Callback: nur die Felder, die du brauchst ----
    156 static static enum GNUNET_GenericReturnValue chat_message_cb(void* cls,
    157                             struct GNUNET_CHAT_Context* ctx,
    158                             struct GNUNET_CHAT_Message* msg) {
    159     auto* sess = static_cast<ChatSession*>(cls);
    160     if (!sess || !sess->cbGlobal || !msg) {
    161         return GNUNET_YES; // niemals Scheduler blockieren
    162     }
    163 
    164     JNIEnv* env = GetEnv();
    165     if (!env) return GNUNET_YES;
    166 
    167     jclass clsCb  = env->GetObjectClass(sess->cbGlobal);
    168     jclass clsCtx = env->FindClass("org/gnunet/gnunetmessenger/ipc/ChatContextDto");
    169     jclass clsMsg = env->FindClass("org/gnunet/gnunetmessenger/ipc/ChatMessageDto");
    170     if (!clsCb || !clsCtx || !clsMsg) return GNUNET_YES;
    171 
    172     jmethodID ctorCtx = env->GetMethodID(clsCtx, "<init>", "()V");
    173     jmethodID ctorMsg = env->GetMethodID(clsMsg, "<init>", "()V");
    174     if (!ctorCtx || !ctorMsg) return GNUNET_YES;
    175 
    176     jobject jCtx = env->NewObject(clsCtx, ctorCtx);
    177     jobject jMsg = env->NewObject(clsMsg, ctorMsg);
    178     if (!jCtx || !jMsg) return GNUNET_YES;
    179 
    180     // -> Kontext füllen
    181     fillChatContextDto(env, jCtx, ctx); // deine Helper-Funktion
    182 
    183     // -> ChatMessageDto-Felder setzen (minimal TEXT/WARNING)
    184     jfieldID fKind      = env->GetFieldID(clsMsg, "kind", "I");
    185     jfieldID fTimestamp = env->GetFieldID(clsMsg, "timestamp", "J");
    186     jfieldID fText      = env->GetFieldID(clsMsg, "text", "Ljava/lang/String;");
    187     if (!fKind || !fTimestamp || !fText) {
    188         env->DeleteLocalRef(jCtx);
    189         env->DeleteLocalRef(jMsg);
    190         return GNUNET_YES;
    191     }
    192 
    193     const int kind = (int) GNUNET_CHAT_message_get_kind(msg);
    194     env->SetIntField(jMsg, fKind, (jint) kind);
    195     env->SetLongField(jMsg, fTimestamp, (jlong) getTimestampMillis(msg)); // dein Helper
    196 
    197     switch (kind) {
    198         case GNUNET_CHAT_KIND_WARNING:
    199         case GNUNET_CHAT_KIND_TEXT: {
    200             const char* t = GNUNET_CHAT_message_get_text(msg);
    201             jstring jT = t ? env->NewStringUTF(t) : nullptr;
    202             env->SetObjectField(jMsg, fText, jT);
    203             if (jT) env->DeleteLocalRef(jT);
    204             break;
    205         }
    206         case GNUNET_CHAT_KIND_INVITATION:
    207         default:
    208             env->SetObjectField(jMsg, fText, (jobject) nullptr);
    209             break;
    210     }
    211 
    212     // Java-Callback: void onMessage(ChatContextDto, ChatMessageDto)
    213     jmethodID mOnMessage = env->GetMethodID(
    214             clsCb,
    215             "onMessage",
    216             "(Lorg/gnunet/gnunetmessenger/ipc/ChatContextDto;"
    217             "Lorg/gnunet/gnunetmessenger/ipc/ChatMessageDto;)V"
    218     );
    219     if (mOnMessage) {
    220         env->CallVoidMethod(sess->cbGlobal, mOnMessage, jCtx, jMsg);
    221     }
    222 
    223     env->DeleteLocalRef(jCtx);
    224     env->DeleteLocalRef(jMsg);
    225     return GNUNET_YES;
    226 }
    227 
    228 
    229 
    230 extern "C" JNIEXPORT jboolean JNICALL
    231 Java_org_gnunet_gnunetmessenger_ipc_NativeBridge_initCfgFromAsset(
    232         JNIEnv* env, jclass,
    233         jobject jAssetManager, jstring jAssetName)
    234 {
    235     if (g_cfgInited.load()) return JNI_TRUE;
    236 
    237     AAssetManager* mgr = AAssetManager_fromJava(env, jAssetManager);
    238     if (!mgr) return JNI_FALSE;
    239 
    240     const char* assetName = env->GetStringUTFChars(jAssetName, nullptr);
    241     AAsset* asset = AAssetManager_open(mgr, assetName, AASSET_MODE_BUFFER);
    242     env->ReleaseStringUTFChars(jAssetName, assetName);
    243     if (!asset) return JNI_FALSE;
    244 
    245     off_t sz = AAsset_getLength(asset);
    246     if (sz <= 0) { AAsset_close(asset); return JNI_FALSE; }
    247 
    248     std::vector<char> buf;
    249     buf.resize(static_cast<size_t>(sz));
    250     const int readBytes = AAsset_read(asset, buf.data(), sz);
    251     AAsset_close(asset);
    252     if (readBytes != sz) return JNI_FALSE;
    253 
    254     // Config-Handle mit Projekt-Daten erzeugen (Android-Build hat die GNUnet-Projektinfos)
    255     struct GNUNET_CONFIGURATION_Handle* cfg =
    256             GNUNET_CONFIGURATION_create(GNUNET_OS_project_data_gnunet());
    257     if (!cfg) return JNI_FALSE;
    258 
    259     // Wichtig: deserialize erwartet die exakte Datenlänge (ohne extra NUL)
    260     if (GNUNET_OK != GNUNET_CONFIGURATION_deserialize(cfg, buf.data(),
    261                                                       static_cast<size_t>(sz), nullptr)) {
    262         GNUNET_CONFIGURATION_destroy(cfg);
    263         return JNI_FALSE;
    264     }
    265 
    266     g_cfg = cfg;
    267     g_cfgInited.store(true);
    268     return JNI_TRUE;
    269 }
    270 
    271 // ======================================================
    272 // JNI: startChat
    273 // Signature in Kotlin/Java (Server):
    274 //     private external fun nativeStartChat(appName: String, callback: IChatCallback): Long
    275 // Package/Cls: org.gnu.gnunet.ipc.GnunetChatIpcService
    276 // ======================================================
    277 
    278 // Ergänzung: zum Freigeben der Config
    279 extern "C" JNIEXPORT void JNICALL
    280 Java_org_gnunet_gnunetmessenger_ipc_NativeBridge_destroyCfg(
    281         JNIEnv*, jclass, jlong cfgPtr) {
    282     auto* cfg = reinterpret_cast<GNUNET_CONFIGURATION_Handle*>(cfgPtr);
    283     if (cfg) GNUNET_CONFIGURATION_destroy(cfg);
    284 }
    285 
    286 // Start mit bereits erstellter cfg
    287 extern "C" JNIEXPORT jlong JNICALL
    288 Java_org_gnunet_gnunetmessenger_ipc_NativeBridge_nativeStartChatUsingCfg(
    289         JNIEnv* env, jclass,
    290         jlong cfgPtr, jstring jAppName, jobject jCallback) {
    291 
    292     const char* appName = env->GetStringUTFChars(jAppName, nullptr);
    293     LOGD("nativeStartChat(%s)", appName ? appName : "(null)");
    294 
    295     // Neue Session anlegen
    296     auto sess = std::make_unique<ChatSession>();
    297     sess->handle = g_nextHandle++;
    298     if (jCallback) {
    299         sess->cbGlobal = env->NewGlobalRef(jCallback); // global ref behalten
    300     }
    301 
    302     // --- GNUnet Chat starten ---
    303     sess->chatHandle = GNUNET_CHAT_start(
    304             reinterpret_cast<const GNUNET_CONFIGURATION_Handle *>(cfgPtr), &chat_message_cb, sess.get());
    305     GNUNET_CONFIGURATION_destroy(reinterpret_cast<GNUNET_CONFIGURATION_Handle *>(cfgPtr));
    306 
    307     if (!sess->chatHandle) {
    308         LOGE("GNUNET_CHAT_start failed");
    309         if (sess->cbGlobal) {
    310             env->DeleteGlobalRef(sess->cbGlobal);
    311             sess->cbGlobal = nullptr;
    312         }
    313         env->ReleaseStringUTFChars(jAppName, appName);
    314         return 0;
    315     }
    316 
    317     // Session registrieren
    318     long handle = (long)sess->handle;
    319     {
    320         std::lock_guard<std::mutex> lk(g_mapMtx);
    321         g_sessions.emplace(handle, std::move(sess));
    322     }
    323 
    324     env->ReleaseStringUTFChars(jAppName, appName);
    325     LOGD("Session created with handle=%ld", handle);
    326     return static_cast<jlong>(handle);
    327 }
    328 
    329 // ======================================================
    330 // JNI: createAccount
    331 // Signature in Kotlin/Java:
    332 //     private external fun nativeCreateAccount(handle: Long, name: String): Int
    333 // ======================================================
    334 
    335 extern "C" JNIEXPORT jint JNICALL
    336 Java_org_gnu_gnunet_ipc_GnunetChatIpcService_nativeCreateAccount(
    337         JNIEnv* env, jobject /*thiz*/, jlong jHandle, jstring jName) {
    338 
    339     const char* name = env->GetStringUTFChars(jName, nullptr);
    340     long h = static_cast<long>(jHandle);
    341     LOGD("nativeCreateAccount(handle=%ld, name=%s)", h, name ? name : "(null)");
    342 
    343     ChatSession* sess = findSessionOrNull(h);
    344     if (!sess || !sess->chatHandle) {
    345         env->ReleaseStringUTFChars(jName, name);
    346         return -1; // GNUNET_SYSERR analog
    347     }
    348 
    349     const char* nameC = env->GetStringUTFChars(jName, nullptr);
    350     if (!nameC) {
    351         LOGE("nativeCreateAccount: failed to get UTF chars");
    352         return GNUNET_SYSERR;
    353     }
    354 
    355     int rc = GNUNET_CHAT_account_create(sess->chatHandle, nameC);
    356 
    357     env->ReleaseStringUTFChars(jName, name);
    358     return rc;
    359 }
    360 
    361 // ======================================================
    362 // JNI: connect
    363 // Signature in Kotlin/Java:
    364 //     private external fun nativeConnect(handle: Long, accountKey: String, accountName: String)
    365 // Du kannst die Parameterform anpassen – aktuell minimal.
    366 // ======================================================
    367 
    368 extern "C" JNIEXPORT void JNICALL
    369 Java_org_gnu_gnunet_ipc_GnunetChatIpcService_nativeConnect(
    370         JNIEnv* env, jobject /*thiz*/, jlong jHandle,
    371         jstring jAccountKey, jstring jAccountName) {
    372 
    373     long h = static_cast<long>(jHandle);
    374     const char* key = jAccountKey ? env->GetStringUTFChars(jAccountKey, nullptr) : "";
    375     const char* name = jAccountName ? env->GetStringUTFChars(jAccountName, nullptr) : "";
    376     struct GNUNET_CHAT_Account *account;
    377     LOGD("nativeConnect(handle=%ld, key=%s, name=%s)", h, key, name);
    378 
    379     ChatSession* sess = findSessionOrNull(h);
    380     if (!sess || !sess->chatHandle) {
    381         if (jAccountKey) env->ReleaseStringUTFChars(jAccountKey, key);
    382         if (jAccountName) env->ReleaseStringUTFChars(jAccountName, name);
    383         return;
    384     }
    385 
    386     account = GNUNET_CHAT_find_account(
    387             sess->chatHandle,
    388             name
    389     );
    390 
    391     GNUNET_CHAT_connect(sess->chatHandle, account);
    392 
    393     sess->connected = true; // Platzhalter
    394 
    395     if (jAccountKey) env->ReleaseStringUTFChars(jAccountKey, key);
    396     if (jAccountName) env->ReleaseStringUTFChars(jAccountName, name);
    397 }
    398 
    399 // ======================================================
    400 // JNI: disconnect
    401 // Signature in Kotlin/Java:
    402 //     private external fun nativeDisconnect(handle: Long)
    403 // ======================================================
    404 
    405 extern "C" JNIEXPORT void JNICALL
    406 Java_org_gnu_gnunet_ipc_GnunetChatIpcService_nativeDisconnect(
    407         JNIEnv* /*env*/, jobject /*thiz*/, jlong jHandle) {
    408 
    409     long h = static_cast<long>(jHandle);
    410     LOGD("nativeDisconnect(handle=%ld)", h);
    411 
    412     ChatSession* sess = findSessionOrNull(h);
    413     if (!sess || !sess->chatHandle) return;
    414 
    415     if (sess->chatHandle) {
    416         GNUNET_CHAT_disconnect(sess->chatHandle); // ✅
    417         sess->chatHandle = nullptr;
    418         sess->connected  = false;
    419     }
    420     sess->connected = false; // Platzhalter
    421 }
    422 
    423 // ======================================================
    424 // JNI: getProfileName
    425 // Signature in Kotlin/Java:
    426 //     private external fun nativeGetProfileName(handle: Long): String
    427 // ======================================================
    428 
    429 extern "C" JNIEXPORT jstring JNICALL
    430 Java_org_gnu_gnunet_ipc_GnunetChatIpcService_nativeGetProfileName(
    431         JNIEnv* env, jobject /*thiz*/, jlong jHandle) {
    432 
    433     long h = static_cast<long>(jHandle);
    434     ChatSession* sess = findSessionOrNull(h);
    435     if (!sess) return env->NewStringUTF("");
    436 
    437     sess->profileName = GNUNET_CHAT_get_name (sess->chatHandle);
    438     return env->NewStringUTF(sess->profileName.c_str());
    439 }
    440 
    441 // ======================================================
    442 // JNI: setProfileName
    443 // Signature in Kotlin/Java:
    444 //     private external fun nativeSetProfileName(handle: Long, name: String)
    445 // ======================================================
    446 
    447 extern "C" JNIEXPORT void JNICALL
    448 Java_org_gnu_gnunet_ipc_GnunetChatIpcService_nativeSetProfileName(
    449         JNIEnv* env, jobject /*thiz*/, jlong jHandle, jstring jName) {
    450 
    451     long h = static_cast<long>(jHandle);
    452     const char* name = env->GetStringUTFChars(jName, nullptr);
    453 
    454     ChatSession* sess = findSessionOrNull(h);
    455     if (sess) {
    456         GNUNET_CHAT_set_name (sess->chatHandle ,name);
    457         sess->profileName = name ? name : "";
    458     }
    459 
    460     env->ReleaseStringUTFChars(jName, name);
    461 }
    462 
    463 // --------- Hilfsfunktion: ChatAccountDto bauen ---------
    464 static jobject buildChatAccountDto(JNIEnv* env,
    465                                    const char* key,
    466                                    const char* name)
    467 {
    468     if (!env) return nullptr;
    469 
    470     jclass clsDto = env->FindClass("org/gnunet/gnunetmessenger/ipc/ChatAccountDto");
    471     if (!clsDto) return nullptr;
    472 
    473     jmethodID ctor = env->GetMethodID(clsDto, "<init>", "()V");
    474     if (!ctor) return nullptr;
    475 
    476     jobject dto = env->NewObject(clsDto, ctor);
    477     if (!dto) return nullptr;
    478 
    479     jfieldID fKey  = env->GetFieldID(clsDto, "key",  "Ljava/lang/String;");
    480     jfieldID fName = env->GetFieldID(clsDto, "name", "Ljava/lang/String;");
    481     if (!fKey || !fName) return dto; // Felder optional setzen
    482 
    483     jstring jKey  = key  ? env->NewStringUTF(key)  : nullptr;
    484     jstring jName = name ? env->NewStringUTF(name) : nullptr;
    485 
    486     env->SetObjectField(dto, fKey,  jKey);
    487     env->SetObjectField(dto, fName, jName);
    488 
    489     if (jKey)  env->DeleteLocalRef(jKey);
    490     if (jName) env->DeleteLocalRef(jName);
    491 
    492     return dto;
    493 }
    494 
    495 // --------- Daten für die Iteration + JNI Callback IDs cachen ---------
    496 struct IterateAccountsCtx {
    497     jclass cbClass = nullptr;
    498     jobject cbGlobal = nullptr;
    499     jmethodID onAccount = nullptr; // (LChatAccountDto;)V
    500     jmethodID onDone    = nullptr; // ()V
    501     jmethodID onError   = nullptr; // (ILjava/lang/String;)V
    502 };
    503 
    504 // --------- GNUnet-Callback: wird je Account aufgerufen ---------
    505 static enum GNUNET_GenericReturnValue
    506 iterateAccountsCb(void* cls,
    507                   GNUNET_CHAT_Handle* /*handle*/,
    508                   GNUNET_CHAT_Account* account)
    509 {
    510     auto* ictx = static_cast<IterateAccountsCtx*>(cls);
    511     if (!ictx) return GNUNET_YES;
    512 
    513     ScopedEnv senv;
    514     JNIEnv* env = senv.get();
    515     if (!env || !ictx->cbGlobal || !ictx->onAccount) return GNUNET_YES;
    516 
    517     const char* name = GNUNET_CHAT_account_get_name(account);
    518     const char* key  = "";          // Platzhalter
    519     //const char* name = "Account";   // Platzhalter
    520 
    521     jobject dto = buildChatAccountDto(env, key, name);
    522     if (dto) {
    523         env->CallVoidMethod(ictx->cbGlobal, ictx->onAccount, dto);
    524         env->DeleteLocalRef(dto);
    525     }
    526     // Weiter iterieren
    527     return GNUNET_YES;
    528 }
    529 
    530 // --------- JNI: nativeIterateAccounts(handle, IAccountCallback) ---------
    531 extern "C" JNIEXPORT void JNICALL
    532 Java_org_gnunet_gnunetmessenger_ipc_NativeBridge_nativeIterateAccounts(
    533         JNIEnv* env, jclass /*clazz*/,
    534         jlong jHandle,
    535         jobject jAccountCallback /* IAccountCallback */)
    536 {
    537     // 1) Session finden
    538     std::unique_ptr<ChatSession>* sessPtr = nullptr;
    539     {
    540         std::lock_guard<std::mutex> lk(g_mapMtx);
    541         auto it = g_sessions.find((long) jHandle);
    542         if (it != g_sessions.end()) {
    543             sessPtr = &it->second;
    544         }
    545     }
    546     if (!sessPtr || !sessPtr->get()) {
    547         // Callback.onError(...) (falls verfügbar), sonst nur loggen
    548         jclass cbCls = env->GetObjectClass(jAccountCallback);
    549         if (cbCls) {
    550             jmethodID onError = env->GetMethodID(cbCls, "onError", "(ILjava/lang/String;)V");
    551             if (onError) {
    552                 jstring jMsg = env->NewStringUTF("No such session");
    553                 env->CallVoidMethod(jAccountCallback, onError, (jint) -1, jMsg);
    554                 if (jMsg) env->DeleteLocalRef(jMsg);
    555             }
    556             env->DeleteLocalRef(cbCls);
    557         }
    558         return;
    559     }
    560 
    561     ChatSession* sess = sessPtr->get();
    562     if (!sess->chatHandle) {
    563         jclass cbCls = env->GetObjectClass(jAccountCallback);
    564         if (cbCls) {
    565             jmethodID onError = env->GetMethodID(cbCls, "onError", "(ILjava/lang/String;)V");
    566             if (onError) {
    567                 jstring jMsg = env->NewStringUTF("Chat not started (chatHandle==null)");
    568                 env->CallVoidMethod(jAccountCallback, onError, (jint) -2, jMsg);
    569                 if (jMsg) env->DeleteLocalRef(jMsg);
    570             }
    571             env->DeleteLocalRef(cbCls);
    572         }
    573         return;
    574     }
    575 
    576     // 2) Callback-Methoden auflösen & GlobalRef halten (falls GNUnet anderen Thread nutzt)
    577     IterateAccountsCtx ictx;
    578 
    579     ictx.cbGlobal = env->NewGlobalRef(jAccountCallback);
    580     if (!ictx.cbGlobal) return;
    581 
    582     ictx.cbClass = (jclass) env->NewGlobalRef(env->GetObjectClass(jAccountCallback));
    583     if (!ictx.cbClass) {
    584         env->DeleteGlobalRef(ictx.cbGlobal);
    585         return;
    586     }
    587 
    588     ictx.onAccount = env->GetMethodID(
    589             ictx.cbClass, "onAccount",
    590             "(Lorg/gnunet/gnunetmessenger/ipc/ChatAccountDto;)V");
    591     ictx.onDone  = env->GetMethodID(ictx.cbClass, "onDone", "()V");
    592     ictx.onError = env->GetMethodID(ictx.cbClass, "onError", "(ILjava/lang/String;)V");
    593 
    594     if (!ictx.onAccount) {
    595         // Minimal: ohne onAccount können wir nichts tun
    596         if (ictx.onError) {
    597             jstring jMsg = env->NewStringUTF("IAccountCallback.onAccount not found");
    598             env->CallVoidMethod(ictx.cbGlobal, ictx.onError, (jint) -3, jMsg);
    599             if (jMsg) env->DeleteLocalRef(jMsg);
    600         }
    601         env->DeleteGlobalRef(ictx.cbClass);
    602         env->DeleteGlobalRef(ictx.cbGlobal);
    603         return;
    604     }
    605 
    606     // 3) Iteration starten (synchron)
    607     int rc = GNUNET_CHAT_iterate_accounts(sess->chatHandle, &iterateAccountsCb, &ictx);
    608 
    609     // 4) Abschluss melden
    610     if (rc < 0) {
    611         if (ictx.onError) {
    612             jstring jMsg = env->NewStringUTF("iterate_accounts failed");
    613             env->CallVoidMethod(ictx.cbGlobal, ictx.onError, (jint) rc, jMsg);
    614             if (jMsg) env->DeleteLocalRef(jMsg);
    615         }
    616     } else {
    617         if (ictx.onDone) {
    618             env->CallVoidMethod(ictx.cbGlobal, ictx.onDone);
    619         }
    620     }
    621 
    622     // 5) Aufräumen
    623     env->DeleteGlobalRef(ictx.cbClass);
    624     env->DeleteGlobalRef(ictx.cbGlobal);
    625 }