GnunetChatIpcService.kt (12699B)
1 package org.gnu.gnunet.ipc 2 3 import android.app.Service 4 import android.content.Intent 5 import android.os.IBinder 6 import android.os.RemoteException 7 import kotlinx.coroutines.* 8 import kotlinx.coroutines.channels.BufferOverflow 9 import kotlinx.coroutines.channels.Channel 10 import org.gnunet.gnunetmessenger.ipc.ChatAccountDto 11 import org.gnunet.gnunetmessenger.ipc.ChatContextDto 12 import org.gnunet.gnunetmessenger.ipc.ChatMessageDto 13 import org.gnunet.gnunetmessenger.ipc.IAccountCallback 14 import org.gnunet.gnunetmessenger.ipc.IChatCallback 15 import org.gnunet.gnunetmessenger.ipc.IGnunetChat 16 import android.util.Log 17 import org.gnu.gnunet.core.NativeBridge 18 19 class GnunetChatIpcService : Service() { 20 21 private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) 22 private val sessions = mutableMapOf<Long, Session>() 23 private var nextHandle = 1L 24 @Volatile private var cfgPtr: Long = 0L 25 26 data class Session( 27 val handle: Long, 28 val cb: IChatCallback, 29 val chan: Channel<Pair<ChatContextDto, ChatMessageDto>>, 30 val worker: Job, 31 var profileName: String = "GNUnet", 32 var connected: Boolean = false, 33 val accounts: MutableList<ChatAccountDto> = mutableListOf() 34 ) 35 36 companion object { 37 private const val TAG = "GnunetChatIpcService" 38 private const val OK = 0 39 private const val ERR_NO_SESSION = 1 40 } 41 42 override fun onCreate() { 43 super.onCreate() 44 // Optional: dauerhaft im Hintergrund 45 // startService(Intent(this, GnunetForegroundService::class.java)) 46 } 47 48 override fun onBind(intent: Intent): IBinder { 49 return object : IGnunetChat.Stub() { 50 override fun getApiVersion(): Int = 100 51 52 override fun startChat(messengerApp: String, cb: IChatCallback): Long { 53 applicationContext.enforceAllowedCaller() // ggf. auskommentieren bis Whitelist steht 54 55 // 1) Channel + Worker: entkoppelt native Events vom Binder zum Client 56 val handle = nextHandle++ // lokaler (Java/Kotlin) Handle – wir überschreiben ihn gleich mit dem nativen, wenn vorhanden 57 val chan = Channel<Pair<ChatContextDto, ChatMessageDto>>( 58 capacity = 256, 59 onBufferOverflow = BufferOverflow.DROP_OLDEST 60 ) 61 val worker = scope.launch(Dispatchers.IO) { 62 for ((ctx, msg) in chan) { 63 val ok = try { 64 cb.onMessage(ctx, msg) // an den echten Client-Binder weiterreichen 65 true 66 } catch (t: Throwable) { 67 Log.e(TAG, "Remote callback to client failed", t) 68 false 69 } 70 if (!ok) { 71 stopSession(handle) // Session aufräumen, wenn Client tot ist 72 break 73 } 74 } 75 } 76 77 // 2) Interner Callback für die NATIVEN Events: schiebt nur in den Channel 78 val nativeCb = object : IChatCallback.Stub() { 79 override fun onMessage(context: ChatContextDto, message: ChatMessageDto) { 80 // leichtgewichtig, kein UI, kein Binder zurück zum Client 81 if (!chan.trySend(context to message).isSuccess) { 82 Log.w(TAG, "Inbound buffer full; dropping oldest") 83 } 84 } 85 } 86 87 val cfg = try { ensureCfg() } 88 catch (t: Throwable) { 89 Log.e(TAG, "ensureCfg failed", t) 90 worker.cancel() 91 chan.close() 92 return 0L 93 } 94 95 // 4) Native Start – nutzt dieselbe Config und registriert unseren internen Callback 96 val nativeHandle = try { 97 NativeBridge.nativeStartChatUsingCfg(cfg, messengerApp, nativeCb) 98 } catch (t: Throwable) { 99 Log.e(TAG, "nativeStartChatUsingCfg failed", t) 100 NativeBridge.destroyCfg(cfg) 101 worker.cancel() 102 chan.close() 103 return 0L 104 } finally { 105 // Falls du die cfg NICHT global wiederverwenden willst, hier freigeben: 106 NativeBridge.destroyCfg(cfg) 107 } 108 109 if (nativeHandle == 0L) { 110 Log.e(TAG, "GNUNET_CHAT_start returned 0 (failed to start)") 111 worker.cancel() 112 chan.close() 113 return 0L 114 } 115 116 // 5) Session registrieren (wir speichern den NATIVEN Handle) 117 sessions[nativeHandle] = Session( 118 handle = nativeHandle, 119 cb = cb, // Binder zum Client 120 chan = chan, // Inbound-Puffer 121 worker = worker // Forwarder-Job 122 // profileName / connected / accounts bleiben wie bei dir 123 ) 124 125 try { 126 cb.asBinder().linkToDeath( 127 { stopSession(handle) }, // wird aufgerufen, wenn Client stirbt 128 0 129 ) 130 } catch (t: RemoteException) { 131 // falls der Binder schon tot war 132 stopSession(handle) 133 } 134 135 Log.d(TAG, "startChat ok -> nativeHandle=$nativeHandle, app=$messengerApp") 136 return nativeHandle 137 } 138 139 override fun iterateAccounts(handle: Long, cb: IAccountCallback) { 140 Log.d(TAG, "iterateAccounts(handle=$handle)") 141 142 applicationContext.enforceAllowedCaller() 143 Log.d(TAG, "hasSession=${sessions.containsKey(handle)}") 144 val sess = sessions[handle] 145 if (sess == null) { 146 cb.onError(404, "Unknown handle") 147 return 148 } 149 150 // Nicht auf dem Main-Thread iterieren 151 scope.launch(Dispatchers.IO) { 152 try { 153 // TODO: Hier die echte GNUnet-Iteration aufrufen. 154 // Fürs erste: Dummy oder aus deiner GNUnet-Brücke lesen. 155 // Beispiel-Dummy: 156 val list = listOf( 157 ChatAccountDto().apply { key="acc-1"; name="Alice" }, 158 ChatAccountDto().apply { key="acc-2"; name="Bob" } 159 ) 160 161 for (dto in list) { 162 try { 163 cb.onAccount(dto) // oneway: non-blocking 164 } catch (t: Throwable) { 165 Log.e(TAG, "iterateAccounts: callback failed", t) 166 cb.onError(500, "callback failed: ${t.message}") 167 return@launch 168 } 169 } 170 cb.onDone() 171 } catch (t: Throwable) { 172 Log.e(TAG, "iterateAccounts failed", t) 173 cb.onError(500, t.message ?: "unknown error") 174 } 175 } 176 } 177 178 179 180 override fun createAccount(handle: Long, name: String): Int { 181 Log.d(TAG, "createAccount(handle=$handle)") 182 applicationContext.enforceAllowedCaller() 183 184 val sess = sessions[handle] ?: return ERR_NO_SESSION 185 186 // Später: echten GNUnet-Account erstellen (JNI) und Ergebnis zurückgeben. 187 // Vorläufig: "Account" lokal vormerken 188 val account = ChatAccountDto().apply { 189 key = "acc:${System.currentTimeMillis()}" 190 this.name = name 191 } 192 sess.accounts += account 193 194 // Beispiel: optional dem Client via Callback signalisieren (nicht zwingend) 195 scope.launch(Dispatchers.IO) { 196 runCatching { 197 val ctx = ChatContextDto().apply { 198 chatContextType = 0; userPointer = null; isGroup = false; isPlatform = false 199 } 200 val msg = ChatMessageDto().apply { 201 text = "Account '$name' erstellt" 202 timestamp = System.currentTimeMillis() 203 kind = 0; type = -1; senderKey = "server" 204 } 205 sess.chan.trySend(ctx to msg) 206 Log.d(TAG, "createAccount send") 207 } 208 } 209 210 return OK 211 } 212 213 override fun connect(handle: Long, account: ChatAccountDto?) { 214 applicationContext.enforceAllowedCaller() 215 val sess = sessions[handle] ?: return 216 // Hier später GNUNET_CHAT_connect(handle, account) aufrufen 217 sess.connected = true 218 // Optional: Event rausschicken 219 val ctx = ChatContextDto().apply { 220 chatContextType = 0; userPointer = null; isGroup = false; isPlatform = false 221 } 222 val msg = ChatMessageDto().apply { 223 text = "Verbunden mit Account: ${account?.name ?: "(null)"}" 224 timestamp = System.currentTimeMillis() 225 kind = 0; type = -1; senderKey = "server" 226 } 227 scope.launch { sess.chan.trySend(ctx to msg) } 228 } 229 230 override fun disconnect(handle: Long) { 231 applicationContext.enforceAllowedCaller() 232 stopSession(handle) // bestehende Utility bei dir, die worker cancelt, chan schließt, Map-Eintrag entfernt 233 } 234 235 override fun getProfileName(handle: Long): String { 236 applicationContext.enforceAllowedCaller() 237 val sess = sessions[handle] ?: return "" 238 return sess.profileName 239 } 240 241 override fun setProfileName(handle: Long, name: String) { 242 applicationContext.enforceAllowedCaller() 243 val sess = sessions[handle] ?: return 244 sess.profileName = name 245 246 // optional: Event an Client 247 scope.launch(Dispatchers.IO) { 248 runCatching { 249 val ctx = ChatContextDto().apply { 250 chatContextType = 0; userPointer = null; isGroup = false; isPlatform = false 251 } 252 val msg = ChatMessageDto().apply { 253 text = "Profilname gesetzt: $name" 254 timestamp = System.currentTimeMillis() 255 kind = 0; type = -1; senderKey = "server" 256 } 257 sess.chan.trySend(ctx to msg) 258 } 259 } 260 } 261 } 262 } 263 264 private fun stopSession(handle: Long) { 265 val sess = sessions.remove(handle) ?: return // idempotent 266 try { 267 // 1) JNI: native Seite trennen & Session freigeben 268 runCatching { NativeBridge.nativeDisconnect(handle) } 269 .onFailure { Log.w(TAG, "nativeDisconnect failed for $handle", it) } 270 271 // 2) Worker/Channel schließen 272 sess.worker.cancel() 273 sess.chan.close() 274 275 } catch (t: Throwable) { 276 Log.w(TAG, "stopSession cleanup error", t) 277 } 278 } 279 280 override fun onDestroy() { 281 val ptr = cfgPtr 282 if (ptr != 0L) { 283 runCatching { NativeBridge.destroyCfg(ptr) } 284 cfgPtr = 0L 285 } 286 sessions.keys.toList().forEach { stopSession(it) } 287 scope.cancel() 288 super.onDestroy() 289 } 290 291 // --- Öffentliche Hilfsfunktion für deine GNUnet-Bridge --- 292 fun emitToHandle(handle: Long, ctx: ChatContextDto, msg: ChatMessageDto) { 293 sessions[handle]?.chan?.trySend(ctx to msg) 294 } 295 296 private fun ensureCfg(): Long { 297 var local = cfgPtr 298 if (local != 0L) return local 299 synchronized(this) { 300 if (cfgPtr == 0L) { 301 cfgPtr = NativeBridge.initCfgFromAsset(applicationContext.assets, "gnunet.conf") 302 require(cfgPtr != 0L) { "initCfgFromAsset failed (cfgPtr==0)" } 303 } 304 return cfgPtr 305 } 306 } 307 }