Files
simplex-chat/apps/multiplatform/spec/architecture.md
2026-02-26 17:54:44 +00:00

27 KiB

System Architecture

Table of Contents

  1. Overview
  2. Module Structure
  3. JNI Bridge
  4. App Lifecycle
  5. Event Streaming
  6. Platform Abstraction
  7. Source Files

1. Overview

The application is a three-layer system:

+------------------------------------------------------------------+
|                     Compose UI (Views)                           |
|  ChatListView, ChatView, ComposeView, SettingsView, CallView    |
+------------------------------------------------------------------+
        |                    ^
        | user actions       | Compose MutableState recomposition
        v                    |
+------------------------------------------------------------------+
|              Application Logic Layer                             |
|  ChatModel (state)   ChatController (command dispatch)           |
|  AppPreferences      NtfManager      ThemeManager                |
+------------------------------------------------------------------+
        |                    ^
        | sendCmd()          | recvMsg() / processReceivedMsg()
        v                    |
+------------------------------------------------------------------+
|                    JNI Bridge (Core.kt)                          |
|  external fun chatSendCmdRetry()    external fun chatRecvMsgWait()|
+------------------------------------------------------------------+
        |                    ^
        | C FFI              | C FFI
        v                    |
+------------------------------------------------------------------+
|              Haskell Core (libsimplex / libapp-lib)              |
|  chat_ctrl handle    SMP/XFTP protocols    SQLite/PostgreSQL     |
+------------------------------------------------------------------+

Data flow summary:

  1. User interacts with Compose UI.
  2. View calls a suspend fun api*() method on ChatController.
  3. ChatController.sendCmd() serializes the command to a JSON string and calls chatSendCmdRetry() (JNI).
  4. The Haskell core processes the command and returns a JSON response string.
  5. The response is deserialized to an API sealed class and returned to the caller.
  6. Asynchronous events from the core (incoming messages, connection updates, call invitations) are delivered via a receiver coroutine that calls chatRecvMsgWait() in a loop and dispatches each event through processReceivedMsg().

2. Module Structure

Gradle Configuration

Root: settings.gradle.kts

include(":android", ":desktop", ":common")

:common Module

Build file: common/build.gradle.kts

kotlin {
    androidTarget()
    jvm("desktop")
}

Source sets:

Source Set Path Purpose
commonMain common/src/commonMain/kotlin/ All shared UI, models, platform abstractions
androidMain common/src/androidMain/kotlin/ Android actual implementations
desktopMain common/src/desktopMain/kotlin/ Desktop actual implementations

Key dependencies (from commonMain):

  • kotlinx-serialization-json -- JSON codec for Haskell core communication
  • kotlinx-datetime -- cross-platform date/time
  • multiplatform-settings (russhwolf) -- SharedPreferences abstraction
  • kaml -- YAML parsing (theme import/export)
  • boofcv-core -- QR code scanning
  • jsoup -- HTML parsing for link previews
  • moko-resources -- cross-platform string/image resources
  • multiplatform-markdown-renderer -- Markdown rendering in chat

:android Module

Build file: android/build.gradle.kts

Contains:

  • SimplexApp (Application subclass)
  • MainActivity (FragmentActivity)
  • SimplexService (foreground Service)
  • NtfManager (Android NotificationManager wrapper)
  • CallActivity (dedicated activity for calls)

:desktop Module

Build file: desktop/build.gradle.kts

Contains:

  • main() entry point
  • initHaskell() -- loads native library and calls initHS()
  • Window management (VLC library loading on Windows)

3. JNI Bridge

All JNI declarations reside in Core.kt.

External Native Functions

# Function Signature Line Purpose
1 initHS() external fun initHS() 18 Initialize GHC runtime system
2 pipeStdOutToSocket() external fun pipeStdOutToSocket(socketName: String): Int 20 Redirect Haskell stdout to Android local socket for logging
3 chatMigrateInit() external fun chatMigrateInit(dbPath: String, dbKey: String, confirm: String): Array<Any> 25 Initialize database with migration; returns [jsonResult, chatCtrl]
4 chatCloseStore() external fun chatCloseStore(ctrl: ChatCtrl): String 26 Close database store
5 chatSendCmdRetry() external fun chatSendCmdRetry(ctrl: ChatCtrl, msg: String, retryNum: Int): String 27 Send command to core with retry count
6 chatSendRemoteCmdRetry() external fun chatSendRemoteCmdRetry(ctrl: ChatCtrl, rhId: Int, msg: String, retryNum: Int): String 28 Send command to remote host
7 chatRecvMsg() external fun chatRecvMsg(ctrl: ChatCtrl): String 29 Receive message (non-blocking)
8 chatRecvMsgWait() external fun chatRecvMsgWait(ctrl: ChatCtrl, timeout: Int): String 30 Receive message with timeout (blocking up to timeout microseconds)
9 chatParseMarkdown() external fun chatParseMarkdown(str: String): String 31 Parse markdown formatting
10 chatParseServer() external fun chatParseServer(str: String): String 32 Parse SMP/XFTP server address
11 chatParseUri() external fun chatParseUri(str: String, safe: Int): String 33 Parse SimpleX connection URI
12 chatPasswordHash() external fun chatPasswordHash(pwd: String, salt: String): String 34 Hash password with salt
13 chatValidName() external fun chatValidName(name: String): String 35 Validate/sanitize display name
14 chatJsonLength() external fun chatJsonLength(str: String): Int 36 Get JSON-encoded string length
15 chatWriteFile() external fun chatWriteFile(ctrl: ChatCtrl, path: String, buffer: ByteBuffer): String 37 Write encrypted file via core
16 chatReadFile() external fun chatReadFile(path: String, key: String, nonce: String): Array<Any> 38 Read and decrypt file
17 chatEncryptFile() external fun chatEncryptFile(ctrl: ChatCtrl, fromPath: String, toPath: String): String 39 Encrypt file on disk
18 chatDecryptFile() external fun chatDecryptFile(fromPath: String, key: String, nonce: String, toPath: String): String 40 Decrypt file on disk

Total: 18 external native functions (the ChatCtrl type alias at line 23 is Long, representing the Haskell-side controller pointer).

Key Kotlin Functions in Core.kt

Function Line Purpose
initChatControllerOnStart() 51 Entry point called during app startup; launches initChatController in a long-running coroutine
initChatController() 62 Main initialization: DB migration via chatMigrateInit, error recovery (incomplete DB removal), sets file paths, loads active user, starts chat
chatInitTemporaryDatabase() 190 Creates a temporary database for migration scenarios
chatInitControllerRemovingDatabases() 202 Removes existing DBs and creates fresh controller (used during re-initialization)
showStartChatAfterRestartAlert() 222 Shows confirmation dialog when chat was stopped and DB passphrase is stored

initChatController Flow

initChatController(useKey, confirmMigrations, startChat)
  |
  +-- chatMigrateInit(dbPath, dbKey, confirm)     // JNI -> Haskell
  |     returns [jsonResult, chatCtrl]
  |
  +-- if migration error and rerunnable:
  |     chatMigrateInit(dbPath, dbKey, confirm)    // retry with user confirmation
  |
  +-- setChatCtrl(ctrl)                            // store controller handle
  +-- apiSetAppFilePaths(...)                       // tell core about file dirs
  +-- apiSetEncryptLocalFiles(...)
  +-- apiGetActiveUser() -> currentUser
  +-- getServerOperators() -> conditions
  +-- if shouldImportAppSettings: apiGetAppSettings + importIntoApp
  +-- if user exists and startChat confirmed:
  |     startChat(user)                            // starts receiver, API commands
  +-- else if no user:
       set onboarding stage, optionally startChatWithoutUser()

4. App Lifecycle

Android

Entry: SimplexApp.onCreate()

SimplexApp.onCreate()
  +-- initHaskell(packageName)           // Load native lib, pipe stdout, call initHS()
  |     +-- System.loadLibrary("app-lib")
  |     +-- pipeStdOutToSocket(packageName)
  |     +-- initHS()
  +-- initMultiplatform()                // Set up ntfManager, platform callbacks
  +-- reconfigureBroadcastReceivers()
  +-- runMigrations()                    // Theme migration, version code tracking
  +-- initChatControllerOnStart()        // -> initChatController() -> chatMigrateInit -> startChat

Activity: MainActivity.onCreate()

MainActivity.onCreate()
  +-- processNotificationIntent(intent)  // Handle OpenChat/AcceptCall from notifications
  +-- processIntent(intent)              // Handle VIEW intents (deep links)
  +-- processExternalIntent(intent)      // Handle SEND/SEND_MULTIPLE (share sheet)
  +-- setContent { AppScreen() }         // Compose UI entry point

Lifecycle callbacks in SimplexApp (implements LifecycleEventObserver):

  • ON_START: refresh chat list from API if chat is running
  • ON_RESUME: show background service notice, start SimplexService if configured

Desktop

Entry: main()

main()
  +-- initHaskell()                      // Load native lib from resources dir, call initHS()
  |     +-- System.load(libapp-lib.so/dll/dylib)
  |     +-- initHS()
  +-- runMigrations()
  +-- setupUpdateChecker()
  +-- initApp()                          // Set ntfManager, applyAppLocale, initChatControllerOnStart
  +-- showApp()                          // Compose window with AppScreen()

showApp() creates a Compose Window with error recovery -- if a crash occurs, it closes the offending modal/view and re-opens the window.

initApp() sets the ntfManager implementation (desktop notifications via NtfManager in common/model/) and calls initChatControllerOnStart().


5. Event Streaming

Receiver Coroutine

ChatController.startReceiver() launches a coroutine on Dispatchers.IO that continuously polls for events from the Haskell core:

// SimpleXAPI.kt line 660
private fun startReceiver() {
    if (receiverJob != null || chatCtrl == null) return  // guard against double-start
    receiverJob = CoroutineScope(Dispatchers.IO).launch {
        var releaseLock: (() -> Unit) = {}
        while (isActive) {
            val ctrl = chatCtrl
            if (ctrl == null) { stopReceiver(); break }  // chatCtrl became null
            try {
                val release = releaseLock
                launch { delay(30000); release() }       // release previous wake lock after 30s
                val msg = recvMsg(ctrl)                   // calls chatRecvMsgWait with 300s timeout
                releaseLock = getWakeLock(timeout = 60000) // acquire wake lock (60s timeout)
                if (msg != null) {
                    val finished = withTimeoutOrNull(60_000L) {
                        processReceivedMsg(msg)
                        messagesChannel.trySend(msg)
                    }
                    if (finished == null) {
                        Log.e(TAG, "Timeout processing: " + msg.responseType)
                    }
                }
            } catch (e: Exception) {
                Log.e(TAG, "recvMsg/processReceivedMsg exception: " + e.stackTraceToString())
            } catch (e: Throwable) {
                Log.e(TAG, "recvMsg/processReceivedMsg throwable: " + e.stackTraceToString())
            }
        }
    }
}

Message Reception

recvMsg() calls chatRecvMsgWait(ctrl, MESSAGE_TIMEOUT) where MESSAGE_TIMEOUT = 300_000_000 microseconds (300 seconds). Returns null on timeout (empty string from Haskell), otherwise deserializes the JSON response to an API instance.

Command Sending

sendCmd() runs on Dispatchers.IO, serializes the command via CC.cmdString, calls chatSendCmdRetry() (or chatSendRemoteCmdRetry() for remote hosts), deserializes the response, and logs terminal items.

Event Processing

processReceivedMsg() is a large when block that dispatches on the CR (ChatResponse) type:

  • CR.ContactConnected -- update contact in ChatModel
  • CR.NewChatItems -- add items to chat, trigger notifications
  • CR.RcvCallInvitation -- add to callInvitations, trigger call UI
  • CR.ChatStopped -- set chatRunning = false
  • CR.GroupMemberConnected, CR.GroupMemberUpdated, etc. -- update group state
  • Many more event types for connection status, file transfers, SMP relay events, etc.

Wake Lock

On Android, the receiver acquires a wake lock via getWakeLock(timeout) (expect function) after each received message with a 60-second timeout. The previous iteration's wake lock is released after a 30-second delay, ensuring overlap so the CPU does not sleep between messages.


6. Platform Abstraction

expect/actual Pattern

The commonMain source set declares expect functions and classes. Each platform source set provides actual implementations.

Examples from platform files:

expect Declaration File Line
expect val appPlatform: AppPlatform AppCommon.kt 20
expect val deviceName: String AppCommon.kt 22
expect fun isAppVisibleAndFocused(): Boolean AppCommon.kt 24
expect val dataDir: File Files.kt 18
expect val tmpDir: File Files.kt 19
expect val filesDir: File Files.kt 20
expect val appFilesDir: File Files.kt 21
expect val dbAbsolutePrefixPath: String Files.kt 24
expect fun showToast(text: String, timeout: Long) UI.kt 6
expect fun hideKeyboard(view: Any?, clearFocus: Boolean) UI.kt 16
expect fun getKeyboardState(): State<KeyboardState> UI.kt 15
expect fun allowedToShowNotification(): Boolean Notifications.kt 3
expect class VideoPlayer VideoPlayer.kt 25
expect class RecorderNative RecAndPlay.kt 17
expect val cryptor: CryptorInterface Cryptor.kt 9
expect fun base64ToBitmap(base64ImageString: String): ImageBitmap Images.kt 17
expect fun getWakeLock(timeout: Long): (() -> Unit) SimplexService.kt 3
expect class GlobalExceptionsHandler UI.kt 24
expect fun UriHandler.sendEmail(subject: String, body: CharSequence) Share.kt 7
expect fun ClipboardManager.shareText(text: String) Share.kt 9
expect fun shareFile(text: String, fileSource: CryptoFile) Share.kt 10

PlatformInterface Callback Object

PlatformInterface is an interface with default no-op implementations. It is assigned at runtime by each platform entry point:

The global variable is declared at Platform.kt line 50:

var platform: PlatformInterface = object : PlatformInterface {}

PlatformInterface Callbacks

Callback Default Android Implementation
androidServiceStart() no-op Start SimplexService foreground service
androidServiceSafeStop() no-op Stop SimplexService
androidCallServiceSafeStop() no-op Stop CallService
androidNotificationsModeChanged(mode) no-op Toggle receivers, start/stop service
androidChatStartedAfterBeingOff() no-op Start service or schedule periodic worker
androidChatStopped() no-op Cancel workers, stop service
androidChatInitializedAndStarted() no-op Show background service notice, start service
androidIsBackgroundCallAllowed() true Check battery restriction
androidSetNightModeIfSupported() no-op Set UiModeManager night mode
androidSetStatusAndNavigationBarAppearance(...) no-op Configure system bar colors/appearance
androidStartCallActivity(acceptCall, rhId, chatId) no-op Launch CallActivity
androidPictureInPictureAllowed() true Check PiP permission via AppOps
androidCallEnded() no-op Destroy call WebView
androidRestartNetworkObserver() no-op Restart NetworkObserver
androidCreateActiveCallState() empty Closeable Create ActiveCallState
androidIsXiaomiDevice() false Check device brand
androidApiLevel null Build.VERSION.SDK_INT
androidLockPortraitOrientation() no-op Lock to SCREEN_ORIENTATION_PORTRAIT
androidAskToAllowBackgroundCalls() true Show battery restriction dialog
desktopShowAppUpdateNotice() no-op Show update notice (Desktop only)

7. Source Files

Core Infrastructure

File Path Key Contents
Core.kt common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt JNI externals, initChatController, chatInitTemporaryDatabase
SimpleXAPI.kt common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt ChatController, AppPreferences, startReceiver, sendCmd, recvMsg, processReceivedMsg, all api* functions
ChatModel.kt common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt ChatModel singleton, ChatsContext, Chat, ChatInfo, ChatItem and all domain types
App.kt common/src/commonMain/kotlin/chat/simplex/common/App.kt AppScreen(), MainScreen()

Platform Layer

File Path Key Contents
Platform.kt common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt PlatformInterface, global platform var
AppCommon.kt common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt AppPlatform, runMigrations()
AppCommon.android.kt common/src/androidMain/kotlin/chat/simplex/common/platform/AppCommon.android.kt initHaskell(), androidAppContext
AppCommon.desktop.kt common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt initApp(), desktop NtfManager setup
Files.kt common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt expect val dataDir/tmpDir/filesDir/dbAbsolutePrefixPath
NtfManager.kt common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt abstract class NtfManager
Notifications.kt common/src/commonMain/kotlin/chat/simplex/common/platform/Notifications.kt expect fun allowedToShowNotification()
UI.kt common/src/commonMain/kotlin/chat/simplex/common/platform/UI.kt showToast, hideKeyboard, GlobalExceptionsHandler
Share.kt common/src/commonMain/kotlin/chat/simplex/common/platform/Share.kt shareText, shareFile, openFile
VideoPlayer.kt common/src/commonMain/kotlin/chat/simplex/common/platform/VideoPlayer.kt VideoPlayerInterface, expect class VideoPlayer
RecAndPlay.kt common/src/commonMain/kotlin/chat/simplex/common/platform/RecAndPlay.kt RecorderInterface, AudioPlayerInterface
Cryptor.kt common/src/commonMain/kotlin/chat/simplex/common/platform/Cryptor.kt CryptorInterface
Images.kt common/src/commonMain/kotlin/chat/simplex/common/platform/Images.kt base64ToBitmap, resizeImageToStrSize
SimplexService.kt common/src/commonMain/kotlin/chat/simplex/common/platform/SimplexService.kt expect fun getWakeLock()

Entry Points

File Path Key Contents
SimplexApp.kt android/src/main/java/chat/simplex/app/SimplexApp.kt Android Application class, lifecycle observer
MainActivity.kt android/src/main/java/chat/simplex/app/MainActivity.kt Android main activity
SimplexService.kt android/src/main/java/chat/simplex/app/SimplexService.kt Android foreground service
Main.kt desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt Desktop main()
DesktopApp.kt common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt showApp(), SimplexWindowState

Theme

File Path Key Contents
ThemeManager.kt common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt Theme resolution, system/light/dark/custom, per-user overrides