Files
simplex-chat/apps/multiplatform/spec/architecture.md
T
fbca2cae81 core, ui: resize images in the core for sharper previews and images (#7941)
* WIP: Haskell impl of resizeImageToStrSize

* WIP: add FFI export wrapper

* WIP: switch ios resizeImageToStrSizeSync to backend

* WIP: fix resizeImageToStrSizeSync

* WIP: add JNI wrappers

* Add non-base64 resize and cWriteImage

* WIP: add chat_write_image

* fix resizeToStr

* fixes

* fix syntax, raise jpeg quality back

* add chatWriteImage

* Fix cfArgs in tests

* Use jpeg-turbo encoder

* add nix pins

* bump jpeg-turbo

* add CI deps

* add encoding funs to android lib

* bump jpeg-turbo for 20.04 fix

* jpeg-turbo fix for 8.10

* android fixes

* Bump jpeg-turbo

* set static flag for jpeg-turbo

* Fix nix for aarch64/android

* Fix android C api

* Fix resize in android

* Bump hs-libjpeg-turbo

* target correct override

* override android lib from android pkgs

* add macos build tweaks

* bump hs-jpeg-turbo for 8.10 fixes

* more build fixes

* make ios compile

* Always use pkgconfig for libjpeg-turbo

* WIP: debug libjpeg in ubuntu pkg-config CI

* Add pkg-config to dockerfiles

* Add libturbojpeg to dockerfiles

* Use -dev version of libturbojpeg in dockerfiles

* Install pkg-config in windows CI

* Disable flag for custom libturbojpeg linking options

Workarounds relocation error on x86_64-linux with gcc:
  /usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/libturbojpeg.a(turbojpeg.c.o): relocation R_X86_64_TPOFF32 against `errStr' can not be used when making a shared object; recompile with -fPIC
  /usr/bin/ld: failed to set dynamic section sizes: bad value

* WIP: debug libjpeg in windows pkg-config CI

* WIP: debug libjpeg package in windows CI

* core: image resize module and buffer FFI

* ci: link libjpeg-turbo statically on all platforms

* ui: resize images in the core

* core: rename image resize functions to chat_resize_image and chat_resize_image_uri

* ci: restore whitespace in build.yml

* ci: build libjpeg-turbo 3.1.4.1 from a submodule

* core: hs-jpeg-turbo from github.com/simplex-chat

* nodejs: resize images

* python: resize images

* core: hs-jpeg-turbo from gitlab.com, the GitHub import lacks LFS objects

* core: generate /img previews

* ci: link libjpeg-turbo on Windows without pkg-config

* ci: build libjpeg-turbo on Windows with the GHC toolchain

* docs: plan for image resizing in the core

* docs: update image resizing plan

* ios: prevent crash when wallpaper image cannot be resized

* android, desktop: raise an error when an image cannot be resized

* docs: update spec references for image resizing

* docker: build the CLI image from a clone without submodules

* docs: apply review fixes to image resizing plan

* docker: build the CLI from a git worktree with the submodule present

* docs: apply second review to image resizing plan

* docs: point image resizing plan at the rewritten commit

* core: choose image format by transparency in chat_resize_image

* nodejs: resize images to the app sizes

* python: resize images to the app sizes

* nodejs, python: describe the core modules as used for image resizing

* docs: add license notices of the image libraries

* ci: describe the avatar method without attributing it to the apps

* ci, ios, nix: remove the libjpeg-turbo build outside cabal

* python: name resize_image in the core module text

* nodejs, python: describe resizing to the app image sizes

* core: refuse images larger than the supported size

* core: resize to JPEG images with a side above 65500 pixels

* docs: record image resizing decisions for the bundled libjpeg-turbo

* core, docs: build libjpeg-turbo and stb from the fork submodules

* docs: record the fork pins in the image resizing plan

* core: resize images to at most 4320 pixels per side, refuse above 16384

* docs: record the image side limits in the image resizing plan

* core: pin the rebased hs-jpeg-turbo fork

* core: pin the hs-jpeg-turbo fork without package.yaml

* docs: plan scaled JPEG decoding and EXIF orientation of images

* docs: copy the JuicyPixels-stbir licence of the pinned fork

* core: pin the hs-jpeg-turbo fork with scaled JPEG decoding

* core: decode JPEG at the display size, up to 65500 pixels per side

* core: rotate JPEG images by their EXIF orientation

* docs: refer the image plan to the JPEG decoding code

* docs: record the approved image processing decisions

* core: limit JPEG decoding memory to 1024 MB outside iOS and Android

* docs: name JPEG decoding in the libjpeg-turbo notice

* docs: plan image resizing from files and the decisions of 26 Oct

* core: scale images in one Catmull-Rom pass with sharpening

* docs: plan image sizes at a fixed quality

* core: resize images at a fixed quality to fill the size limit

* core: resize images from a file path or a file descriptor

* ios, android, desktop: resize picked images from their files

* core: encode resized images with mozjpeg

* tests: check descriptor closing by the file it refers to

* core: raise JPEG quality only for a full-size image below 97% of the limit

* ci: name the mozjpeg submodule in the Windows cleanup comments

* docs: refer the image plan to the current code

* docs: refer the image plan to the code merged with master

* core: export every image resizing function from each library

* core: close the file descriptor when its handle cannot be created

* docs: count the C sources of mozjpeg with the arithmetic decoder

* core: use the reviewed hs-jpeg-turbo and JuicyPixels-stbir forks

* ci: build the armv7a Android core library on request

* core: round image heights half up in the size search

* ios: describe the file image resizing functions in SimpleX.h

* ci: pick, preview and send images in the Claude app tests

* core: decode PNG images with libspng and refuse other formats

* core: encode PNG with libspng, keeping the smaller filter choice

* ci: pick, preview and send images in the scripted app smoke tests

* flake: build the PNG decoder package in nix builds

* ios: remove the copy of a picked image after sending it

* core: refuse image files larger than the decode memory limit

* core: scale a JPEG image to its largest size once per resize

* core: bisect JPEG quality without a step function

* core: reduce the memory used to sharpen and to decode PNG images

* android, desktop: pass the image format to the core as an integer

* android, desktop: fuse the preview conditions of picked media

* android, desktop, ci: check that the core resized the smoke test images

* python: remove the docstrings of the explicit image resizing functions

* docs: add the second review fixes to the image plan

* ci: tap the iOS attachment button by its accessibility label

* core: keep the colour profiles of resized images

* docs: add the colour profiles to the image plan

* ci: wait up to 30 s for the new group screens in the iOS smoke test

* ci: pick the Android smoke test images from their own photo folders

* ci: tap the iOS photo picker by position, as its tree is not shown

* ci: pick the test card and the logo by their iOS picker positions

* core: update JuicyPixels-stbir with the stb_image_resize licence terms

* core: write a fixed Display P3 profile for Display P3 images only

* core: do not keep the input image in memory for its colour profile

* core: read the JPEG segments once for the orientation and the profile

* core: read an image file into one buffer of its size

* docs: add the third review fixes to the image plan

* core: resize images with Display P3 or no profile, not profile bytes

* ci: check the pixel sizes of the images sent in the iOS smoke test

* core: update hs-jpeg-turbo with PNG chunk limits and encoder fixes

* docs: list Compact ICC Profiles, libspng and miniz in the dependencies

* android, desktop: reword the picked media preview comment

* docs: refer the image plan to the code merged with master

* ci: render the iOS snapshots before the images case

* ci: dismiss the snapshot tests' camera alert before the iOS images case

* core: update hs-jpeg-turbo with tests and the JuicyPixels-spng synopsis

* core: pin hs-jpeg-turbo and JuicyPixels-stbir in simplex-chat repositories

* update ios core library

* core, ui: accept only JPEG and PNG image data

* core: update JuicyPixels-stbir

* docs: plan for the iOS copy of picked images

---------

Co-authored-by: IC Rainbow <aenor.realm@gmail.com>
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
Co-authored-by: Alexandre Esteves <alexfmpe@proton.me>
2026-10-30 12:14:51 +00:00

31 KiB
Raw Blame History

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 chatParseBadgeCode() external fun chatParseBadgeCode(code: String): String 36 Canonical form of a badge code, or an empty string
15 chatJsonLength() external fun chatJsonLength(str: String): Int 37 Get JSON-encoded string length
16 chatWriteFile() external fun chatWriteFile(ctrl: ChatCtrl, path: String, buffer: ByteBuffer): String 38 Write encrypted file via core
17 chatReadFile() external fun chatReadFile(path: String, key: String, nonce: String): Array<Any> 39 Read and decrypt file
18 chatEncryptFile() external fun chatEncryptFile(ctrl: ChatCtrl, fromPath: String, toPath: String): String 40 Encrypt file on disk
19 chatDecryptFile() external fun chatDecryptFile(fromPath: String, key: String, nonce: String, toPath: String): String 41 Decrypt file on disk
20 chatResizeImageUri() external fun chatResizeImageUri(buffer: ByteBuffer, maxSize: Int): String 42 Resize encoded image to a data URI of at most maxSize characters
21 chatResizeImage() external fun chatResizeImage(buffer: ByteBuffer, maxSize: Int, png: Int): String 43 Resize encoded image to base64 of PNG or JPEG data of at most maxSize bytes
22 chatResizeImageFileUri() external fun chatResizeImageFileUri(path: String, maxSize: Int): String 44 Desktop: as chatResizeImageUri, from the file at path
23 chatResizeImageFile() external fun chatResizeImageFile(path: String, maxSize: Int, png: Int): String 45 Desktop: as chatResizeImage, from the file at path; png −1 selects the format by transparency
24 chatResizeImageFdUri() external fun chatResizeImageFdUri(fd: Int, maxSize: Int): String 46 Android: as chatResizeImageUri, from the file descriptor fd, which core closes
25 chatResizeImageFd() external fun chatResizeImageFd(fd: Int, maxSize: Int, png: Int): String 47 Android: as chatResizeImage, from the file descriptor fd, which core closes

Total: 25 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(args)
  +-- linkFromArgs(args)                 // A simplexchat: or simplex: link passed by the OS
  +-- acquireSingleInstance(link)        // Or forward the link to the running instance and exit
  +-- appOpenUrl = link                  // Stored before the watcher can forward a newer link
  +-- startShowFileWatcher()             // Only while holding the single-instance lock
  +-- initHaskell()                      // Load native lib from resources dir, call initHS()
  |     +-- System.load(libapp-lib.so/dll/dylib)
  |     +-- initHS()
  +-- runMigrations()
  +-- setupUpdateChecker()
  +-- initApp()                          // Set ntfManager, applyAppLocale, initChatControllerOnStart
  +-- registerLinkSchemes()              // Background thread, per-OS registration and detection
  +-- installOpenUriHandler()            // macOS only: links arrive as Apple Events
  +-- 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.

showWindow() un-minimizes and raises the window when a second launch or a link reaches the running app. On Linux it also sends _NET_ACTIVE_WINDOW through requestX11Activation(), because wlroots compositors such as Sway ignore the raise that AWT's toFront() sends under XWayland. On Windows the second process, which the browser left holding the foreground right, calls AllowSetForegroundWindow before it signals so that the running instance may raise its window. During a crash restart showWindow() skips the disposed window, so it does not come back; the new window opens visible.

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 18
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
androidIsPlayStoreBuild false BuildConfig.PLAY_STORE
androidLoadPlayStoreCountry() no-op Request the Play account country (google flavor only)
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
SingleInstance.kt common/src/desktopMain/kotlin/chat/simplex/common/SingleInstance.kt acquireSingleInstance(), startShowFileWatcher(), signal file hand-over
WindowActivation.kt common/src/desktopMain/kotlin/chat/simplex/common/WindowActivation.kt requestX11Activation(), sendActivation(), activationEvent()

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