commit 443ffeca1e28188f7fbb70777ff884ea800ea835 parent 7a8d45dd66725c035234ef05dde44b7cb32cbb76 Author: t3sserakt <t3sserakt@posteo.de> Date: Wed, 26 Nov 2025 19:54:54 +0100 First attempt to implement remote interfaces. Diffstat:
11 files changed, 1166 insertions(+), 2 deletions(-)
diff --git a/.gitignore b/.gitignore @@ -32,3 +32,5 @@ google-services.json # Android Profiling *.hprof + +**/.DS_Store diff --git a/android_studio/app/build.gradle.kts b/android_studio/app/build.gradle.kts @@ -44,6 +44,7 @@ android { } buildFeatures { viewBinding = true + aidl = true } } @@ -56,4 +57,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/android_studio/app/src/main/AndroidManifest.xml b/android_studio/app/src/main/AndroidManifest.xml @@ -2,9 +2,13 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> + <!-- bestehende Berechtigungen --> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> + <!-- nötig, wenn du den Foreground-Service nutzt --> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> + <application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" @@ -16,15 +20,38 @@ android:theme="@style/Theme.GNUnet" android:allowNativeHeapPointerTagging="false" tools:targetApi="34"> + <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> - <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> + + <!-- ✳️ Öffentlicher Bound Service für den IPC-Contract --> + <service + android:name=".ipc.GnunetChatIpcService" + android:exported="true" + android:enabled="true"> + <intent-filter> + <!-- Muss exakt zum Client-Intent passen --> + <action android:name="org.gnunet.gnunetmessenger.ipc.BIND_GNUNET_CHAT" /> + </intent-filter> + </service> + + <!-- 🔧 Optional: Foreground-Service, falls der Server im Hintergrund + dauerhaft laufen soll (API 26+ Background-Limits) --> + <service + android:name=".ipc.GnunetForegroundService" + android:exported="false" + android:foregroundServiceType="dataSync" /> + // AndroidManifest (Server) + <service + android:name=".core.GnunetCoreService" + android:exported="false" + android:foregroundServiceType="dataSync" /> </application> </manifest> \ No newline at end of file diff --git a/android_studio/app/src/main/cpp/CMakeLists.txt b/android_studio/app/src/main/cpp/CMakeLists.txt @@ -115,6 +115,7 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") add_library(gnunetti SHARED # List C/C++ source files with relative paths to this CMakeLists.txt. native-lib.cpp + GnunetChatIpcBridge.cpp ) target_include_directories(gnunetti PRIVATE diff --git a/android_studio/app/src/main/cpp/GnunetChatIpcBridge.cpp b/android_studio/app/src/main/cpp/GnunetChatIpcBridge.cpp @@ -0,0 +1,625 @@ +#include <jni.h> +#include <map> +#include <mutex> +#include <memory> +#include <atomic> +#include <string> +#include <android/log.h> +#include <android/asset_manager.h> +#include <android/asset_manager_jni.h> +#include "gnunet_chat_lib.h" +#include "gnunet_util_lib.h" + + +#define LOG_TAG "GnunetChatIpcBridge" +#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) +#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) + +// ====================================================== +// Globale Verwaltung +// ====================================================== + +static JavaVM* g_vm = nullptr; + +// Session-Map: handle (jlong) -> ChatSession +static std::mutex g_mapMtx; +struct ChatSession; +static std::map<long, std::unique_ptr<ChatSession>> g_sessions; +static std::atomic_long g_nextHandle{1}; + +static const struct GNUNET_CONFIGURATION_Handle* g_cfg = nullptr; +static std::atomic_bool g_cfgInited{false}; + +// Hilfsfunktion: Thread-sicheres JNIEnv-Beschaffen + optionales Detach +struct ScopedEnv { + JNIEnv* env = nullptr; + bool didAttach = false; + + ScopedEnv() { + if (!g_vm) return; + if (g_vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK || !env) { + if (g_vm->AttachCurrentThread(&env, nullptr) == JNI_OK) { + didAttach = true; + } else { + env = nullptr; + } + } + } + ~ScopedEnv() { + if (didAttach && g_vm) { + g_vm->DetachCurrentThread(); + } + } + JNIEnv* get() const { return env; } +}; + +// ====================================================== +// Session-Struktur mit GNUnet-Handle +// ====================================================== + +struct ChatSession { + jlong handle = 0; + // Optional: Java-Callback-Objekt (IChatCallback Implementierung aus deinem Service) + jobject cbGlobal = nullptr; + + // Beispielhafte Session-Daten + std::string profileName = "GNUnet"; + bool connected = false; + + // GNUnet Chat Handle + struct GNUNET_CHAT_Handle* chatHandle = nullptr; + + ChatSession() = default; + + ~ChatSession() { + // GlobalRef bereinigen + if (cbGlobal) { + ScopedEnv senv; + if (auto* env = senv.get()) { + env->DeleteGlobalRef(cbGlobal); + } + cbGlobal = nullptr; + } + + // GNUnet Chat stoppen + if (chatHandle) { + LOGD("Stopping GNUNET_CHAT handle for session %ld", (long)handle); + GNUNET_CHAT_disconnect(chatHandle); + chatHandle = nullptr; + } + } +}; + +// Helper: Session lookup +static ChatSession* findSessionOrNull(long h) { + std::lock_guard<std::mutex> lk(g_mapMtx); + auto it = g_sessions.find(h); + return (it == g_sessions.end()) ? nullptr : it->second.get(); +} + +// ====================================================== +// JNI Setup +// ====================================================== + +extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) { + g_vm = vm; + return JNI_VERSION_1_6; +} + +// ====================================================== +// GNUnet Callback (eingehende Nachrichten) +// -> Beispiel: ruft eine Methode am Java-Callback auf (onMessageReceived(String)) +// ====================================================== + +static JNIEnv* GetEnv() { + JNIEnv* env = nullptr; + if (g_vm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) { + if (g_vm->AttachCurrentThread(&env, nullptr) != JNI_OK) return nullptr; + } + return env; +} + +// ---- kleine Helfer ---- +static void fillChatContextDto(JNIEnv* env, jobject jCtx, struct GNUNET_CHAT_Context* ctx) { + if (!env || !jCtx) return; + jclass cls = env->GetObjectClass(jCtx); + if (!cls) return; + + jfieldID fType = env->GetFieldID(cls, "chatContextType", "I"); + jfieldID fIsGroup = env->GetFieldID(cls, "isGroup", "Z"); + jfieldID fIsPlat = env->GetFieldID(cls, "isPlatform", "Z"); + jfieldID fUserPtr = env->GetFieldID(cls, "userPointer", "Ljava/lang/String;"); + + int type = 0; + bool isGroup = false; + bool isPlatform = false; + // Heuristik: Ist eine Gruppe am Context? + if (ctx) { + if (GNUNET_CHAT_context_get_group(ctx)) { isGroup = true; type = 1; } + // platform: falls du später einen Indikator hast → hier setzen + } + + env->SetIntField(jCtx, fType, (jint) type); + env->SetBooleanField(jCtx, fIsGroup, (jboolean) isGroup); + env->SetBooleanField(jCtx, fIsPlat, (jboolean) isPlatform); + env->SetObjectField(jCtx, fUserPtr, nullptr); +} + +static jlong getTimestampMillis(struct GNUNET_CHAT_Message* msg) { + if (!msg) return 0; + time_t ts = GNUNET_CHAT_message_get_timestamp(msg); + if (ts <= 0) return 0; + return (jlong) ts * 1000LL; +} + +// ---- Haupt-Callback: nur die Felder, die du brauchst ---- +static static enum GNUNET_GenericReturnValue chat_message_cb(void* cls, + struct GNUNET_CHAT_Context* ctx, + struct GNUNET_CHAT_Message* msg) { + auto* sess = static_cast<ChatSession*>(cls); + if (!sess || !sess->cbGlobal || !msg) { + return GNUNET_YES; // niemals Scheduler blockieren + } + + JNIEnv* env = GetEnv(); + if (!env) return GNUNET_YES; + + jclass clsCb = env->GetObjectClass(sess->cbGlobal); + jclass clsCtx = env->FindClass("org/gnunet/gnunetmessenger/ipc/ChatContextDto"); + jclass clsMsg = env->FindClass("org/gnunet/gnunetmessenger/ipc/ChatMessageDto"); + if (!clsCb || !clsCtx || !clsMsg) return GNUNET_YES; + + jmethodID ctorCtx = env->GetMethodID(clsCtx, "<init>", "()V"); + jmethodID ctorMsg = env->GetMethodID(clsMsg, "<init>", "()V"); + if (!ctorCtx || !ctorMsg) return GNUNET_YES; + + jobject jCtx = env->NewObject(clsCtx, ctorCtx); + jobject jMsg = env->NewObject(clsMsg, ctorMsg); + if (!jCtx || !jMsg) return GNUNET_YES; + + // -> Kontext füllen + fillChatContextDto(env, jCtx, ctx); // deine Helper-Funktion + + // -> ChatMessageDto-Felder setzen (minimal TEXT/WARNING) + jfieldID fKind = env->GetFieldID(clsMsg, "kind", "I"); + jfieldID fTimestamp = env->GetFieldID(clsMsg, "timestamp", "J"); + jfieldID fText = env->GetFieldID(clsMsg, "text", "Ljava/lang/String;"); + if (!fKind || !fTimestamp || !fText) { + env->DeleteLocalRef(jCtx); + env->DeleteLocalRef(jMsg); + return GNUNET_YES; + } + + const int kind = (int) GNUNET_CHAT_message_get_kind(msg); + env->SetIntField(jMsg, fKind, (jint) kind); + env->SetLongField(jMsg, fTimestamp, (jlong) getTimestampMillis(msg)); // dein Helper + + switch (kind) { + case GNUNET_CHAT_KIND_WARNING: + case GNUNET_CHAT_KIND_TEXT: { + const char* t = GNUNET_CHAT_message_get_text(msg); + jstring jT = t ? env->NewStringUTF(t) : nullptr; + env->SetObjectField(jMsg, fText, jT); + if (jT) env->DeleteLocalRef(jT); + break; + } + case GNUNET_CHAT_KIND_INVITATION: + default: + env->SetObjectField(jMsg, fText, (jobject) nullptr); + break; + } + + // Java-Callback: void onMessage(ChatContextDto, ChatMessageDto) + jmethodID mOnMessage = env->GetMethodID( + clsCb, + "onMessage", + "(Lorg/gnunet/gnunetmessenger/ipc/ChatContextDto;" + "Lorg/gnunet/gnunetmessenger/ipc/ChatMessageDto;)V" + ); + if (mOnMessage) { + env->CallVoidMethod(sess->cbGlobal, mOnMessage, jCtx, jMsg); + } + + env->DeleteLocalRef(jCtx); + env->DeleteLocalRef(jMsg); + return GNUNET_YES; +} + + + +extern "C" JNIEXPORT jboolean JNICALL +Java_org_gnunet_gnunetmessenger_ipc_NativeBridge_initCfgFromAsset( + JNIEnv* env, jclass, + jobject jAssetManager, jstring jAssetName) +{ + if (g_cfgInited.load()) return JNI_TRUE; + + AAssetManager* mgr = AAssetManager_fromJava(env, jAssetManager); + if (!mgr) return JNI_FALSE; + + const char* assetName = env->GetStringUTFChars(jAssetName, nullptr); + AAsset* asset = AAssetManager_open(mgr, assetName, AASSET_MODE_BUFFER); + env->ReleaseStringUTFChars(jAssetName, assetName); + if (!asset) return JNI_FALSE; + + off_t sz = AAsset_getLength(asset); + if (sz <= 0) { AAsset_close(asset); return JNI_FALSE; } + + std::vector<char> buf; + buf.resize(static_cast<size_t>(sz)); + const int readBytes = AAsset_read(asset, buf.data(), sz); + AAsset_close(asset); + if (readBytes != sz) return JNI_FALSE; + + // Config-Handle mit Projekt-Daten erzeugen (Android-Build hat die GNUnet-Projektinfos) + struct GNUNET_CONFIGURATION_Handle* cfg = + GNUNET_CONFIGURATION_create(GNUNET_OS_project_data_gnunet()); + if (!cfg) return JNI_FALSE; + + // Wichtig: deserialize erwartet die exakte Datenlänge (ohne extra NUL) + if (GNUNET_OK != GNUNET_CONFIGURATION_deserialize(cfg, buf.data(), + static_cast<size_t>(sz), nullptr)) { + GNUNET_CONFIGURATION_destroy(cfg); + return JNI_FALSE; + } + + g_cfg = cfg; + g_cfgInited.store(true); + return JNI_TRUE; +} + +// ====================================================== +// JNI: startChat +// Signature in Kotlin/Java (Server): +// private external fun nativeStartChat(appName: String, callback: IChatCallback): Long +// Package/Cls: org.gnu.gnunet.ipc.GnunetChatIpcService +// ====================================================== + +// Ergänzung: zum Freigeben der Config +extern "C" JNIEXPORT void JNICALL +Java_org_gnunet_gnunetmessenger_ipc_NativeBridge_destroyCfg( + JNIEnv*, jclass, jlong cfgPtr) { + auto* cfg = reinterpret_cast<GNUNET_CONFIGURATION_Handle*>(cfgPtr); + if (cfg) GNUNET_CONFIGURATION_destroy(cfg); +} + +// Start mit bereits erstellter cfg +extern "C" JNIEXPORT jlong JNICALL +Java_org_gnunet_gnunetmessenger_ipc_NativeBridge_nativeStartChatUsingCfg( + JNIEnv* env, jclass, + jlong cfgPtr, jstring jAppName, jobject jCallback) { + + const char* appName = env->GetStringUTFChars(jAppName, nullptr); + LOGD("nativeStartChat(%s)", appName ? appName : "(null)"); + + // Neue Session anlegen + auto sess = std::make_unique<ChatSession>(); + sess->handle = g_nextHandle++; + if (jCallback) { + sess->cbGlobal = env->NewGlobalRef(jCallback); // global ref behalten + } + + // --- GNUnet Chat starten --- + sess->chatHandle = GNUNET_CHAT_start( + reinterpret_cast<const GNUNET_CONFIGURATION_Handle *>(cfgPtr), &chat_message_cb, sess.get()); + GNUNET_CONFIGURATION_destroy(reinterpret_cast<GNUNET_CONFIGURATION_Handle *>(cfgPtr)); + + if (!sess->chatHandle) { + LOGE("GNUNET_CHAT_start failed"); + if (sess->cbGlobal) { + env->DeleteGlobalRef(sess->cbGlobal); + sess->cbGlobal = nullptr; + } + env->ReleaseStringUTFChars(jAppName, appName); + return 0; + } + + // Session registrieren + long handle = (long)sess->handle; + { + std::lock_guard<std::mutex> lk(g_mapMtx); + g_sessions.emplace(handle, std::move(sess)); + } + + env->ReleaseStringUTFChars(jAppName, appName); + LOGD("Session created with handle=%ld", handle); + return static_cast<jlong>(handle); +} + +// ====================================================== +// JNI: createAccount +// Signature in Kotlin/Java: +// private external fun nativeCreateAccount(handle: Long, name: String): Int +// ====================================================== + +extern "C" JNIEXPORT jint JNICALL +Java_org_gnu_gnunet_ipc_GnunetChatIpcService_nativeCreateAccount( + JNIEnv* env, jobject /*thiz*/, jlong jHandle, jstring jName) { + + const char* name = env->GetStringUTFChars(jName, nullptr); + long h = static_cast<long>(jHandle); + LOGD("nativeCreateAccount(handle=%ld, name=%s)", h, name ? name : "(null)"); + + ChatSession* sess = findSessionOrNull(h); + if (!sess || !sess->chatHandle) { + env->ReleaseStringUTFChars(jName, name); + return -1; // GNUNET_SYSERR analog + } + + const char* nameC = env->GetStringUTFChars(jName, nullptr); + if (!nameC) { + LOGE("nativeCreateAccount: failed to get UTF chars"); + return GNUNET_SYSERR; + } + + int rc = GNUNET_CHAT_account_create(sess->chatHandle, nameC); + + env->ReleaseStringUTFChars(jName, name); + return rc; +} + +// ====================================================== +// JNI: connect +// Signature in Kotlin/Java: +// private external fun nativeConnect(handle: Long, accountKey: String, accountName: String) +// Du kannst die Parameterform anpassen – aktuell minimal. +// ====================================================== + +extern "C" JNIEXPORT void JNICALL +Java_org_gnu_gnunet_ipc_GnunetChatIpcService_nativeConnect( + JNIEnv* env, jobject /*thiz*/, jlong jHandle, + jstring jAccountKey, jstring jAccountName) { + + long h = static_cast<long>(jHandle); + const char* key = jAccountKey ? env->GetStringUTFChars(jAccountKey, nullptr) : ""; + const char* name = jAccountName ? env->GetStringUTFChars(jAccountName, nullptr) : ""; + struct GNUNET_CHAT_Account *account; + LOGD("nativeConnect(handle=%ld, key=%s, name=%s)", h, key, name); + + ChatSession* sess = findSessionOrNull(h); + if (!sess || !sess->chatHandle) { + if (jAccountKey) env->ReleaseStringUTFChars(jAccountKey, key); + if (jAccountName) env->ReleaseStringUTFChars(jAccountName, name); + return; + } + + account = GNUNET_CHAT_find_account( + sess->chatHandle, + name + ); + + GNUNET_CHAT_connect(sess->chatHandle, account); + + sess->connected = true; // Platzhalter + + if (jAccountKey) env->ReleaseStringUTFChars(jAccountKey, key); + if (jAccountName) env->ReleaseStringUTFChars(jAccountName, name); +} + +// ====================================================== +// JNI: disconnect +// Signature in Kotlin/Java: +// private external fun nativeDisconnect(handle: Long) +// ====================================================== + +extern "C" JNIEXPORT void JNICALL +Java_org_gnu_gnunet_ipc_GnunetChatIpcService_nativeDisconnect( + JNIEnv* /*env*/, jobject /*thiz*/, jlong jHandle) { + + long h = static_cast<long>(jHandle); + LOGD("nativeDisconnect(handle=%ld)", h); + + ChatSession* sess = findSessionOrNull(h); + if (!sess || !sess->chatHandle) return; + + if (sess->chatHandle) { + GNUNET_CHAT_disconnect(sess->chatHandle); // ✅ + sess->chatHandle = nullptr; + sess->connected = false; + } + sess->connected = false; // Platzhalter +} + +// ====================================================== +// JNI: getProfileName +// Signature in Kotlin/Java: +// private external fun nativeGetProfileName(handle: Long): String +// ====================================================== + +extern "C" JNIEXPORT jstring JNICALL +Java_org_gnu_gnunet_ipc_GnunetChatIpcService_nativeGetProfileName( + JNIEnv* env, jobject /*thiz*/, jlong jHandle) { + + long h = static_cast<long>(jHandle); + ChatSession* sess = findSessionOrNull(h); + if (!sess) return env->NewStringUTF(""); + + sess->profileName = GNUNET_CHAT_get_name (sess->chatHandle); + return env->NewStringUTF(sess->profileName.c_str()); +} + +// ====================================================== +// JNI: setProfileName +// Signature in Kotlin/Java: +// private external fun nativeSetProfileName(handle: Long, name: String) +// ====================================================== + +extern "C" JNIEXPORT void JNICALL +Java_org_gnu_gnunet_ipc_GnunetChatIpcService_nativeSetProfileName( + JNIEnv* env, jobject /*thiz*/, jlong jHandle, jstring jName) { + + long h = static_cast<long>(jHandle); + const char* name = env->GetStringUTFChars(jName, nullptr); + + ChatSession* sess = findSessionOrNull(h); + if (sess) { + GNUNET_CHAT_set_name (sess->chatHandle ,name); + sess->profileName = name ? name : ""; + } + + env->ReleaseStringUTFChars(jName, name); +} + +// --------- Hilfsfunktion: ChatAccountDto bauen --------- +static jobject buildChatAccountDto(JNIEnv* env, + const char* key, + const char* name) +{ + if (!env) return nullptr; + + jclass clsDto = env->FindClass("org/gnunet/gnunetmessenger/ipc/ChatAccountDto"); + if (!clsDto) return nullptr; + + jmethodID ctor = env->GetMethodID(clsDto, "<init>", "()V"); + if (!ctor) return nullptr; + + jobject dto = env->NewObject(clsDto, ctor); + if (!dto) return nullptr; + + jfieldID fKey = env->GetFieldID(clsDto, "key", "Ljava/lang/String;"); + jfieldID fName = env->GetFieldID(clsDto, "name", "Ljava/lang/String;"); + if (!fKey || !fName) return dto; // Felder optional setzen + + jstring jKey = key ? env->NewStringUTF(key) : nullptr; + jstring jName = name ? env->NewStringUTF(name) : nullptr; + + env->SetObjectField(dto, fKey, jKey); + env->SetObjectField(dto, fName, jName); + + if (jKey) env->DeleteLocalRef(jKey); + if (jName) env->DeleteLocalRef(jName); + + return dto; +} + +// --------- Daten für die Iteration + JNI Callback IDs cachen --------- +struct IterateAccountsCtx { + jclass cbClass = nullptr; + jobject cbGlobal = nullptr; + jmethodID onAccount = nullptr; // (LChatAccountDto;)V + jmethodID onDone = nullptr; // ()V + jmethodID onError = nullptr; // (ILjava/lang/String;)V +}; + +// --------- GNUnet-Callback: wird je Account aufgerufen --------- +static enum GNUNET_GenericReturnValue +iterateAccountsCb(void* cls, + GNUNET_CHAT_Handle* /*handle*/, + GNUNET_CHAT_Account* account) +{ + auto* ictx = static_cast<IterateAccountsCtx*>(cls); + if (!ictx) return GNUNET_YES; + + ScopedEnv senv; + JNIEnv* env = senv.get(); + if (!env || !ictx->cbGlobal || !ictx->onAccount) return GNUNET_YES; + + const char* name = GNUNET_CHAT_account_get_name(account); + const char* key = ""; // Platzhalter + //const char* name = "Account"; // Platzhalter + + jobject dto = buildChatAccountDto(env, key, name); + if (dto) { + env->CallVoidMethod(ictx->cbGlobal, ictx->onAccount, dto); + env->DeleteLocalRef(dto); + } + // Weiter iterieren + return GNUNET_YES; +} + +// --------- JNI: nativeIterateAccounts(handle, IAccountCallback) --------- +extern "C" JNIEXPORT void JNICALL +Java_org_gnunet_gnunetmessenger_ipc_NativeBridge_nativeIterateAccounts( + JNIEnv* env, jclass /*clazz*/, + jlong jHandle, + jobject jAccountCallback /* IAccountCallback */) +{ + // 1) Session finden + std::unique_ptr<ChatSession>* sessPtr = nullptr; + { + std::lock_guard<std::mutex> lk(g_mapMtx); + auto it = g_sessions.find((long) jHandle); + if (it != g_sessions.end()) { + sessPtr = &it->second; + } + } + if (!sessPtr || !sessPtr->get()) { + // Callback.onError(...) (falls verfügbar), sonst nur loggen + jclass cbCls = env->GetObjectClass(jAccountCallback); + if (cbCls) { + jmethodID onError = env->GetMethodID(cbCls, "onError", "(ILjava/lang/String;)V"); + if (onError) { + jstring jMsg = env->NewStringUTF("No such session"); + env->CallVoidMethod(jAccountCallback, onError, (jint) -1, jMsg); + if (jMsg) env->DeleteLocalRef(jMsg); + } + env->DeleteLocalRef(cbCls); + } + return; + } + + ChatSession* sess = sessPtr->get(); + if (!sess->chatHandle) { + jclass cbCls = env->GetObjectClass(jAccountCallback); + if (cbCls) { + jmethodID onError = env->GetMethodID(cbCls, "onError", "(ILjava/lang/String;)V"); + if (onError) { + jstring jMsg = env->NewStringUTF("Chat not started (chatHandle==null)"); + env->CallVoidMethod(jAccountCallback, onError, (jint) -2, jMsg); + if (jMsg) env->DeleteLocalRef(jMsg); + } + env->DeleteLocalRef(cbCls); + } + return; + } + + // 2) Callback-Methoden auflösen & GlobalRef halten (falls GNUnet anderen Thread nutzt) + IterateAccountsCtx ictx; + + ictx.cbGlobal = env->NewGlobalRef(jAccountCallback); + if (!ictx.cbGlobal) return; + + ictx.cbClass = (jclass) env->NewGlobalRef(env->GetObjectClass(jAccountCallback)); + if (!ictx.cbClass) { + env->DeleteGlobalRef(ictx.cbGlobal); + return; + } + + ictx.onAccount = env->GetMethodID( + ictx.cbClass, "onAccount", + "(Lorg/gnunet/gnunetmessenger/ipc/ChatAccountDto;)V"); + ictx.onDone = env->GetMethodID(ictx.cbClass, "onDone", "()V"); + ictx.onError = env->GetMethodID(ictx.cbClass, "onError", "(ILjava/lang/String;)V"); + + if (!ictx.onAccount) { + // Minimal: ohne onAccount können wir nichts tun + if (ictx.onError) { + jstring jMsg = env->NewStringUTF("IAccountCallback.onAccount not found"); + env->CallVoidMethod(ictx.cbGlobal, ictx.onError, (jint) -3, jMsg); + if (jMsg) env->DeleteLocalRef(jMsg); + } + env->DeleteGlobalRef(ictx.cbClass); + env->DeleteGlobalRef(ictx.cbGlobal); + return; + } + + // 3) Iteration starten (synchron) + int rc = GNUNET_CHAT_iterate_accounts(sess->chatHandle, &iterateAccountsCb, &ictx); + + // 4) Abschluss melden + if (rc < 0) { + if (ictx.onError) { + jstring jMsg = env->NewStringUTF("iterate_accounts failed"); + env->CallVoidMethod(ictx.cbGlobal, ictx.onError, (jint) rc, jMsg); + if (jMsg) env->DeleteLocalRef(jMsg); + } + } else { + if (ictx.onDone) { + env->CallVoidMethod(ictx.cbGlobal, ictx.onDone); + } + } + + // 5) Aufräumen + env->DeleteGlobalRef(ictx.cbClass); + env->DeleteGlobalRef(ictx.cbGlobal); +} +\ No newline at end of file diff --git a/android_studio/app/src/main/java/org/gnu/gnunet/MainActivity.kt b/android_studio/app/src/main/java/org/gnu/gnunet/MainActivity.kt @@ -1,9 +1,12 @@ package org.gnu.gnunet +import android.content.Intent import android.content.res.AssetManager +import android.os.Build import android.os.Bundle import android.util.Log import androidx.appcompat.app.AppCompatActivity +import org.gnu.gnunet.core.GnunetCoreService import org.gnu.gnunet.databinding.ActivityMainBinding import java.io.File import java.io.FileOutputStream @@ -51,7 +54,13 @@ class MainActivity : AppCompatActivity() { Log.d("GNUNET", "is asset null? assets = $assets") // Example of a call to a native method - binding.sampleText.text = stringFromJNI(assets, dataPath) + val intent = Intent(this, GnunetCoreService::class.java) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(intent) + } else { + startService(intent) + } + //binding.sampleText.text = stringFromJNI(assets, dataPath) } private fun copyFileFromAssetsToInternalStorage(fileName: String, outputFileName: String): Boolean { diff --git a/android_studio/app/src/main/java/org/gnu/gnunet/core/GnunetCoreService.kt b/android_studio/app/src/main/java/org/gnu/gnunet/core/GnunetCoreService.kt @@ -0,0 +1,119 @@ +package org.gnu.gnunet.core + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.Context +import android.content.Intent +import android.content.res.AssetManager +import android.os.Build +import android.os.Handler +import android.os.HandlerThread +import android.os.IBinder +import android.util.Log +import androidx.core.app.NotificationCompat +import org.gnu.gnunet.R +import org.gnu.gnunet.MainActivity +import org.gnunet.gnunetmessenger.ipc.IAccountCallback +import org.gnunet.gnunetmessenger.ipc.IChatCallback + + +object NativeBridge { + init { + System.loadLibrary("gnunet") + } + external fun stringFromJNI(assets: AssetManager, path: String): String + @JvmStatic external fun initCfgFromAsset(assetManager: AssetManager, assetName: String): Long + @JvmStatic external fun destroyCfg(cfgPtr: Long) + @JvmStatic external fun nativeStartChatUsingCfg(cfgPtr: Long, appName: String, cb: IChatCallback): Long + @JvmStatic external fun nativeDisconnect(sessionHandle: Long) + @JvmStatic external fun nativeGetProfileName(sessionHandle: Long): String + @JvmStatic external fun nativeSetProfileName(sessionHandle: Long, name: String) + @JvmStatic external fun nativeCreateAccount(sessionHandle: Long, name: String): Int + @JvmStatic external fun nativeConnect(sessionHandle: Long, accountKey: String, accountName: String) + @JvmStatic external fun nativeIterateAccounts(sessionHandle: Long, cb: IAccountCallback) +} + +class GnunetCoreService : Service() { + + companion object { + private const val TAG = "GnunetCoreService" + private const val CHANNEL_ID = "gnunet_core_channel" + private const val NOTIF_ID = 1001 + } + + private lateinit var thread: HandlerThread + private lateinit var handler: Handler + + override fun onCreate() { + super.onCreate() + createNotificationChannel() + + // Vordergrunddienst starten (ab Android 8 Pflicht, wenn dauerhaft läuft) + startForeground(NOTIF_ID, buildOngoingNotification()) + + // Eigener Looper-Thread für GNUnet + thread = HandlerThread("gnunet-core") + thread.start() + handler = Handler(thread.looper) + + // GNUnet-Start in den Worker-Thread verschieben (niemals im UI-Thread!) + handler.post { + try { + val res = NativeBridge.stringFromJNI(assets, filesDir.absolutePath) + Log.d(TAG, "GNUnet started: $res") + } catch (t: Throwable) { + Log.e(TAG, "GNUnet start failed", t) + stopSelf() + } + } + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + // dauerhaft laufen lassen + return START_STICKY + } + + override fun onDestroy() { + super.onDestroy() + if (this::thread.isInitialized) { + thread.quitSafely() + } + } + + override fun onBind(intent: Intent?): IBinder? = null + + // ---- Notification Helfer ---- + + private fun buildOngoingNotification(): Notification { + // Optional: Tap öffnet deine App + val contentIntent = PendingIntent.getActivity( + this, + 0, + Intent(this, MainActivity::class.java), + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) + + return NotificationCompat.Builder(this, CHANNEL_ID) + .setSmallIcon(R.mipmap.ic_launcher) // eigenes Icon verwenden + .setContentTitle("GNUnet läuft") + .setContentText("Der GNUnet-Dienst ist aktiv.") + .setOngoing(true) + .setContentIntent(contentIntent) + .build() + } + + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val mgr = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val ch = NotificationChannel( + CHANNEL_ID, + "GNUnet Core", + NotificationManager.IMPORTANCE_LOW + ) + mgr.createNotificationChannel(ch) + } + } +} +\ No newline at end of file diff --git a/android_studio/app/src/main/java/org/gnu/gnunet/ipc/GnunetChatIpcService.kt b/android_studio/app/src/main/java/org/gnu/gnunet/ipc/GnunetChatIpcService.kt @@ -0,0 +1,307 @@ +package org.gnu.gnunet.ipc + +import android.app.Service +import android.content.Intent +import android.os.IBinder +import android.os.RemoteException +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.channels.Channel +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 android.util.Log +import org.gnu.gnunet.core.NativeBridge + +class GnunetChatIpcService : Service() { + + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + private val sessions = mutableMapOf<Long, Session>() + private var nextHandle = 1L + @Volatile private var cfgPtr: Long = 0L + + data class Session( + val handle: Long, + val cb: IChatCallback, + val chan: Channel<Pair<ChatContextDto, ChatMessageDto>>, + val worker: Job, + var profileName: String = "GNUnet", + var connected: Boolean = false, + val accounts: MutableList<ChatAccountDto> = mutableListOf() + ) + + companion object { + private const val TAG = "GnunetChatIpcService" + private const val OK = 0 + private const val ERR_NO_SESSION = 1 + } + + override fun onCreate() { + super.onCreate() + // Optional: dauerhaft im Hintergrund + // startService(Intent(this, GnunetForegroundService::class.java)) + } + + override fun onBind(intent: Intent): IBinder { + return object : IGnunetChat.Stub() { + override fun getApiVersion(): Int = 100 + + override fun startChat(messengerApp: String, cb: IChatCallback): Long { + applicationContext.enforceAllowedCaller() // ggf. auskommentieren bis Whitelist steht + + // 1) Channel + Worker: entkoppelt native Events vom Binder zum Client + val handle = nextHandle++ // lokaler (Java/Kotlin) Handle – wir überschreiben ihn gleich mit dem nativen, wenn vorhanden + val chan = Channel<Pair<ChatContextDto, ChatMessageDto>>( + capacity = 256, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + val worker = scope.launch(Dispatchers.IO) { + for ((ctx, msg) in chan) { + val ok = try { + cb.onMessage(ctx, msg) // an den echten Client-Binder weiterreichen + true + } catch (t: Throwable) { + Log.e(TAG, "Remote callback to client failed", t) + false + } + if (!ok) { + stopSession(handle) // Session aufräumen, wenn Client tot ist + break + } + } + } + + // 2) Interner Callback für die NATIVEN Events: schiebt nur in den Channel + val nativeCb = object : IChatCallback.Stub() { + override fun onMessage(context: ChatContextDto, message: ChatMessageDto) { + // leichtgewichtig, kein UI, kein Binder zurück zum Client + if (!chan.trySend(context to message).isSuccess) { + Log.w(TAG, "Inbound buffer full; dropping oldest") + } + } + } + + val cfg = try { ensureCfg() } + catch (t: Throwable) { + Log.e(TAG, "ensureCfg failed", t) + worker.cancel() + chan.close() + return 0L + } + + // 4) Native Start – nutzt dieselbe Config und registriert unseren internen Callback + val nativeHandle = try { + NativeBridge.nativeStartChatUsingCfg(cfg, messengerApp, nativeCb) + } catch (t: Throwable) { + Log.e(TAG, "nativeStartChatUsingCfg failed", t) + NativeBridge.destroyCfg(cfg) + worker.cancel() + chan.close() + return 0L + } finally { + // Falls du die cfg NICHT global wiederverwenden willst, hier freigeben: + NativeBridge.destroyCfg(cfg) + } + + if (nativeHandle == 0L) { + Log.e(TAG, "GNUNET_CHAT_start returned 0 (failed to start)") + worker.cancel() + chan.close() + return 0L + } + + // 5) Session registrieren (wir speichern den NATIVEN Handle) + sessions[nativeHandle] = Session( + handle = nativeHandle, + cb = cb, // Binder zum Client + chan = chan, // Inbound-Puffer + worker = worker // Forwarder-Job + // profileName / connected / accounts bleiben wie bei dir + ) + + try { + cb.asBinder().linkToDeath( + { stopSession(handle) }, // wird aufgerufen, wenn Client stirbt + 0 + ) + } catch (t: RemoteException) { + // falls der Binder schon tot war + stopSession(handle) + } + + Log.d(TAG, "startChat ok -> nativeHandle=$nativeHandle, app=$messengerApp") + return nativeHandle + } + + override fun iterateAccounts(handle: Long, cb: IAccountCallback) { + Log.d(TAG, "iterateAccounts(handle=$handle)") + + applicationContext.enforceAllowedCaller() + Log.d(TAG, "hasSession=${sessions.containsKey(handle)}") + val sess = sessions[handle] + if (sess == null) { + cb.onError(404, "Unknown handle") + return + } + + // Nicht auf dem Main-Thread iterieren + scope.launch(Dispatchers.IO) { + try { + // TODO: Hier die echte GNUnet-Iteration aufrufen. + // Fürs erste: Dummy oder aus deiner GNUnet-Brücke lesen. + // Beispiel-Dummy: + val list = listOf( + ChatAccountDto().apply { key="acc-1"; name="Alice" }, + ChatAccountDto().apply { key="acc-2"; name="Bob" } + ) + + for (dto in list) { + try { + cb.onAccount(dto) // oneway: non-blocking + } catch (t: Throwable) { + Log.e(TAG, "iterateAccounts: callback failed", t) + cb.onError(500, "callback failed: ${t.message}") + return@launch + } + } + cb.onDone() + } catch (t: Throwable) { + Log.e(TAG, "iterateAccounts failed", t) + cb.onError(500, t.message ?: "unknown error") + } + } + } + + + + override fun createAccount(handle: Long, name: String): Int { + Log.d(TAG, "createAccount(handle=$handle)") + applicationContext.enforceAllowedCaller() + + val sess = sessions[handle] ?: return ERR_NO_SESSION + + // Später: echten GNUnet-Account erstellen (JNI) und Ergebnis zurückgeben. + // Vorläufig: "Account" lokal vormerken + val account = ChatAccountDto().apply { + key = "acc:${System.currentTimeMillis()}" + this.name = name + } + sess.accounts += account + + // Beispiel: optional dem Client via Callback signalisieren (nicht zwingend) + scope.launch(Dispatchers.IO) { + runCatching { + val ctx = ChatContextDto().apply { + chatContextType = 0; userPointer = null; isGroup = false; isPlatform = false + } + val msg = ChatMessageDto().apply { + text = "Account '$name' erstellt" + timestamp = System.currentTimeMillis() + kind = 0; type = -1; senderKey = "server" + } + sess.chan.trySend(ctx to msg) + Log.d(TAG, "createAccount send") + } + } + + return OK + } + + override fun connect(handle: Long, account: ChatAccountDto?) { + applicationContext.enforceAllowedCaller() + val sess = sessions[handle] ?: return + // Hier später GNUNET_CHAT_connect(handle, account) aufrufen + sess.connected = true + // Optional: Event rausschicken + val ctx = ChatContextDto().apply { + chatContextType = 0; userPointer = null; isGroup = false; isPlatform = false + } + val msg = ChatMessageDto().apply { + text = "Verbunden mit Account: ${account?.name ?: "(null)"}" + timestamp = System.currentTimeMillis() + kind = 0; type = -1; senderKey = "server" + } + scope.launch { sess.chan.trySend(ctx to msg) } + } + + override fun disconnect(handle: Long) { + applicationContext.enforceAllowedCaller() + stopSession(handle) // bestehende Utility bei dir, die worker cancelt, chan schließt, Map-Eintrag entfernt + } + + override fun getProfileName(handle: Long): String { + applicationContext.enforceAllowedCaller() + val sess = sessions[handle] ?: return "" + return sess.profileName + } + + override fun setProfileName(handle: Long, name: String) { + applicationContext.enforceAllowedCaller() + val sess = sessions[handle] ?: return + sess.profileName = name + + // optional: Event an Client + scope.launch(Dispatchers.IO) { + runCatching { + val ctx = ChatContextDto().apply { + chatContextType = 0; userPointer = null; isGroup = false; isPlatform = false + } + val msg = ChatMessageDto().apply { + text = "Profilname gesetzt: $name" + timestamp = System.currentTimeMillis() + kind = 0; type = -1; senderKey = "server" + } + sess.chan.trySend(ctx to msg) + } + } + } + } + } + + private fun stopSession(handle: Long) { + val sess = sessions.remove(handle) ?: return // idempotent + try { + // 1) JNI: native Seite trennen & Session freigeben + runCatching { NativeBridge.nativeDisconnect(handle) } + .onFailure { Log.w(TAG, "nativeDisconnect failed for $handle", it) } + + // 2) Worker/Channel schließen + sess.worker.cancel() + sess.chan.close() + + } catch (t: Throwable) { + Log.w(TAG, "stopSession cleanup error", t) + } + } + + override fun onDestroy() { + val ptr = cfgPtr + if (ptr != 0L) { + runCatching { NativeBridge.destroyCfg(ptr) } + cfgPtr = 0L + } + sessions.keys.toList().forEach { stopSession(it) } + scope.cancel() + super.onDestroy() + } + + // --- Öffentliche Hilfsfunktion für deine GNUnet-Bridge --- + fun emitToHandle(handle: Long, ctx: ChatContextDto, msg: ChatMessageDto) { + sessions[handle]?.chan?.trySend(ctx to msg) + } + + private fun ensureCfg(): Long { + var local = cfgPtr + if (local != 0L) return local + synchronized(this) { + if (cfgPtr == 0L) { + cfgPtr = NativeBridge.initCfgFromAsset(applicationContext.assets, "gnunet.conf") + require(cfgPtr != 0L) { "initCfgFromAsset failed (cfgPtr==0)" } + } + return cfgPtr + } + } +} +\ No newline at end of file diff --git a/android_studio/app/src/main/java/org/gnu/gnunet/ipc/GnunetForegroundService.kt b/android_studio/app/src/main/java/org/gnu/gnunet/ipc/GnunetForegroundService.kt @@ -0,0 +1,24 @@ +package org.gnu.gnunet.ipc + +import android.app.* +import android.content.Intent +import android.os.Build +import android.os.IBinder +import androidx.core.app.NotificationCompat + +class GnunetForegroundService : Service() { + override fun onCreate() { + super.onCreate() + if (Build.VERSION.SDK_INT >= 26) { + getSystemService(NotificationManager::class.java) + .createNotificationChannel(NotificationChannel("gnunet_srv","GNUnet", NotificationManager.IMPORTANCE_LOW)) + } + val notif = NotificationCompat.Builder(this, "gnunet_srv") + .setContentTitle("GNUnet läuft") + .setContentText("Bereit für Client-Verbindungen") + .setSmallIcon(android.R.drawable.stat_sys_download_done) + .build() + startForeground(1, notif) + } + override fun onBind(intent: Intent?): IBinder? = null +} +\ No newline at end of file diff --git a/android_studio/app/src/main/java/org/gnu/gnunet/ipc/Security.kt b/android_studio/app/src/main/java/org/gnu/gnunet/ipc/Security.kt @@ -0,0 +1,41 @@ +package org.gnu.gnunet.ipc + +import android.content.Context +import android.content.pm.PackageManager +import android.os.Binder +import android.util.Base64 +import java.security.MessageDigest +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate + +private val allowedPackages = setOf("org.gnunet.gnunetmessenger") + +// SHA-256 der Signing-Zertifikate erlaubter Clients (Base64 der DER-Bytes) +private val ALLOWED_CERT_SHA256: Set<String> = setOf( + // TODO: hier echten Fingerprint der Client-App(s) eintragen +) + +private fun X509Certificate.sha256Base64(): String { + val md = MessageDigest.getInstance("SHA-256") + return Base64.encodeToString(md.digest(encoded), Base64.NO_WRAP) +} + +private fun Context.packageCertsBase64(pkg: String): List<String> { + val info = packageManager.getPackageInfo(pkg, PackageManager.GET_SIGNING_CERTIFICATES) + val cf = CertificateFactory.getInstance("X.509") + return info.signingInfo.apkContentsSigners.map { sig -> + val cert = cf.generateCertificate(sig.toByteArray().inputStream()) as X509Certificate + cert.sha256Base64() + } +} + +fun Context.enforceAllowedCaller() { + val uid = Binder.getCallingUid() + val pkgs = packageManager.getPackagesForUid(uid)?.toList().orEmpty() + if (pkgs.isEmpty()) throw SecurityException("No package for uid=$uid") + else if ((pkgs intersect allowedPackages).isEmpty()) { + throw SecurityException("Caller not allowed. uid=$uid pkgs=$pkgs") + } + /*val ok = pkgs.any { pkg -> runCatching { packageCertsBase64(pkg) }.getOrNull()?.any { it in ALLOWED_CERT_SHA256 } == true } + if (!ok) throw SecurityException("Caller not allowed. uid=$uid pkgs=$pkgs")*/ +} +\ No newline at end of file diff --git a/android_studio/settings.gradle.kts b/android_studio/settings.gradle.kts @@ -16,6 +16,7 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + mavenLocal() } }