gnunet-android

GNUnet for Android
Log | Files | Refs | README

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 }