Android¶
New to Android development? Android, as Sioul uses it explains the words this page uses (activities, services, receivers, processes, JNI), and how the APK is built and signed.
Sioul runs on Android: the same window, built for 64-bit ARM phones running Android 9 or later, installed by hand from the APK on the releases page. Each version is signed with Sioul's key, so the next one installs over it, the phone's data kept. It is not in a store, and parts of Sioul do not work there (below).
Building it¶
On GitHub: .github/workflows/android.yml runs on each push to main that touches the code or android/, and by hand (Actions, "Android", "Run workflow"). The APK is the run's artifact, sioul-android-arm64. For a version, the packages' workflow (.github/workflows/release.yml) calls the same workflow on the version's tag and puts the APK on the release, Sioul-<version>-android-arm64.apk. Both sign it with Sioul's key, kept in the repository's secrets (ANDROID_KEYSTORE, the keystore in base64, and ANDROID_KEYSTORE_PASSWORD); without them, with a key made for that run alone, which a phone takes for another app.
By hand, with:
- Qt for Android 6.11 (
android_arm64_v8a) with Qt Multimedia, Qt Positioning, Qt Location, Qt Image Formats and Qt SerialPort, and the desktop Qt of the same version, whose tools build for Android; - the Android SDK with the NDK Qt 6.11 is built with (r27c,
27.2.12479018), a platform (android-36) and its build tools; - Java 17 or newer, for Gradle;
- Rust with the phone's target:
rustup target add aarch64-linux-android.
<Qt for Android>/bin/qt-cmake -S android -B build-android -G Ninja \
-DCMAKE_BUILD_TYPE=Release -DQT_HOST_PATH=<desktop Qt> \
-DANDROID_SDK_ROOT=<SDK> -DANDROID_NDK_ROOT=<NDK> \
-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON
cmake --build build-android --target apk
build-android/…/outputs/apk/release/; apksigner (the SDK's build tools) signs it, as the workflow does.
Checked on a computer: android/jvm-checks/run.sh compiles the Java against android-36 and runs its pure parts on a computer's JVM against what Rust decides: who rings (DecideCheck, TableCheck), a number's key (KeyCheck), the log of calls screened (LogCheck), Sioul's modes, an event's own alarm and a conversation's key (ModeCheck), do-not-disturb heard both ways (HeardCheck), what the full card on the home screen says at a time (CardCheck, with the card Rust's test writes where SIOUL_CARD_SAMPLE says), the multimedia messages Rust writes read by Android's own parser (MmsCheck: AOSP's PduParser and the classes it needs, copied unchanged from platform/frameworks/base at android16-release into android/jvm-checks/aosp/, Apache-2.0 with its NOTICE, compiled apart with stand-ins for the four Android classes they name, never part of Sioul's program; texts.md); then javadoc, every warning an error. It needs a JDK and android.jar, no phone; org.json is fetched the first time and checked by its SHA-256. What Android does with the result is tried on a phone, each section says when.
How it is made¶
- The window is a library (
crates/sioul-app/src/lib.rs,run()): the desktop program (main.rs) calls it, and so does Android's (android/main.cpp, throughsioul_app_run). - Qt for Android builds the app around it with CMake (
android/CMakeLists.txt): Corrosion builds the crate with Cargo as a static library for the phone (OVERRIDE_CRATE_TYPE, so that desktop builds make the program alone); it is linked withmain.cppinto the library Qt's Java side loads,libsioul_arm64-v8a.so;androiddeployqtand Gradle make the APK, with the QML modules the pages import. - CXX-Qt's part is done by hand. CXX-Qt's own CMake function (
cxx_qt_import_crate) names a crate's export folder after its library (sioul_app), and its build script after its package (sioul-app): the CMake file sets the build script's variables itself and links the two object files it leaves (the QML module's registration). - What a desktop gives a program and Android does not,
main.cppsets before the window starts (or before a dose's alarm, below): the XDG folders, in the app's private storage (configuration, data and state among its files, the cache where Android may empty it);LANG, from the phone's language;SSL_CERT_DIR, the system's certificates; stdout and stderr sent to logcat (adb logcat -s sioul); and the JavaVM and the application's Context, lent to Rust (sioul_sync::android::init). - The Gradle build (
android/package/build.gradle) is Qt 6.11.2's template, whichandroiddeployqttakes from Sioul's package folder instead of its own, with one change: Android's resources are kept in English and French (resConfigs "en", "fr"), where Qt's keeps English alone and droppedres/values-fr(the tiles' and the widgets' French names). It is compared with Qt's own at each Qt update. - The manifest (
android/package/) is Qt 6.11's own with Sioul's name, its icon (the quill on its green, in Android's vector drawings, fromtools/make-icons.py), the network permissions, and no cloud backup: mail, notes, health readings and passwords stay on the phone. Moving to a new phone (cable or Wi-Fi between the two) takes everything but the passwords. - 16 KB memory pages: Android 15 runs on phones whose memory pages are 16 KB, where a library aligned for 4 KB does not load. The library is linked for them, and the workflow checks each library of the APK (its summary lists them).
Starting¶
On a 2019 phone (Gigaset GS290, Helio P23, Android 12), Sioul's window draws its first frame about 1.8 s after a tap: 0.55 s before Sioul's own code runs (Android starts the app, Qt's Java side loads Qt's libraries and Sioul's), then 1.25 s. It was 3.5 s. adb logcat -s sioul says where the time goes ("Sioul: the window loaded after … ms"); on a computer, SIOUL_TIMING=1.
- Made when needed: the pages but the one shown, the places' drawer (at the first ☰,
qml/Places.qml), the Reader on the Porch, the menus, dialogs, settings panels and pop-ups, and the windows (writing, focus, routines). Those made by their file (Loader.setSource,Qt.createComponent) are not even read at the start. - Read once, beside: while the QML engine reads the window's files, a thread reads the system's fonts (200 on a phone) and the image formats (
cpp/warmup.cpp). - Looked up less: Qt's file selectors off (
QT_NO_BUILTIN_SELECTORS), the icon theme in four folders instead of eighteen, Sioul Symbols for the symbols Roboto lacks (no search through the phone's fonts), the Basic style for Qt's own tips (Material not loaded). - Linked lighter: Sioul's library exports 36 symbols, not 28,000, and its relocations are packed (
--exclude-libs, RELR:android/CMakeLists.txt). - What remains is Qt's: relocating Qt's own libraries (0.3 s), registering the types of Qt Quick and its controls (0.3 s), and the media and position plugins Qt's Java side loads at the start, FFmpeg with them, since their
JNI_OnLoadmust run from Java. - Measured with
simpleperf(the manifest makes the app profileable from a computer plugged in) and Qt's QML profiler (qmlprofiler, withQQmlDebuggingEnabler::startTcpDebugServerin a build of one's own, never shipped).
Pages and scrolling¶
On the same phone each page now opens in 0.1 to 0.55 s the first time (it was 0.5 to 1 s, and five seconds for Accounts), then at once. What took the time was Qt making objects nobody had asked for yet; Sioul's own calls from the pages cost under 2 %.
- Made at first use: every form, confirmation and context menu of a page (
qml/Later.qml:form.now().open(),form.close()); a date field's month, when its picker opens (each month is 42 days worked out in local time); the mail reader with the first message; a contact's card and form; a budget's detail; the sharing panel with its tab; the tasks' "now" view not while the rest cover hides it. An id inside aLateris not seen outside it: the page reaches it throughnow()or a property of its own. - Lists reuse their rows while scrolling (
reuseItems); icons are read on a thread. Scrolling a long list costs the window's thread about a second of work per eight seconds of swiping (contacts), the others less. - A trap of Qt 6.11: whenever an item is shown, hidden or restacked (a reused row, a line that appears), Qt walks the window's whole tree at the next frame (
QQuickWindowPrivate::updateChildWindowStackingOrder). The fewer objects made, the less every such change costs. - Touch: Qt's tap handlers ignore which button they accept when the press is a finger, so a right-click handler would open its menu at every tap. Right clicks are taken from a mouse or a touchpad only; on a touch screen a long press opens the same menu, where the finger is (
main.qml'smenuAt,qml/SioulMenu.qml), and letting go then opens nothing. - Away: what the window draws with (about 60 MB of graphics) is given back while Sioul is in the background (
sioul_android_lean_windowinmain.cpp,releaseResourcesas Sioul goes away) and made again when it comes back, in under a tenth of a second. Sioul then holds about 140 MB in the background, 200 on the screen. - Measured with the window's log (
adb logcat | grep sioul-perf: "TasksPage.qml made in … ms") and the scripts kept beside the phone build (opening every page from the menu; the window's and the drawing thread's work while swiping, from/proc;simpleperffor the detail).
What differs on Android¶
- Sites: Qt WebEngine has no Android version. The Sites page lists your sites and opens each in the browser (
qml/android/SitesPage.qml): no logins kept in Sioul. Their notifications are the browser's, held until their time like any other app's when Sioul may see notifications (Notifications from other apps). - PDFs: Qt PDF has no Android version either; a PDF of the notes opens with "Open with…" (
qml/android/PdfView.qml). Both pages keep the names of the computer's, so the module'sqmldirfinds them (build.rslists one or the other). - Passwords go to Android's KeyStore, through android-keyring: a key the KeyStore keeps, and never lets out, encrypts each password (AES-GCM) into the app's private storage. Its author calls it experimental.
- Sender checks ask the network's DNS servers, which Android gives through Java (hickory reads them with the Context lent at start).
- Certificates: Android's own (its Conscrypt module, else the system image's), for IMAP and SMTP alike. On computers SMTP checks them with the system's verifier (
rustls-platform-verifier), which on Android needs Java code Sioul does not ship. - The window on a phone: below 720 pixels wide, the places are pulled over the pages from the left (☰) and each page shows one pane at a time, its list or what it opened; Android's Back goes back from what is open, then to the Porch. Upright, every page fits the screen's width: titles go under the arrows, buttons keep their icons alone, rows that do not fit wrap or go one under the other (the board's columns, an account's address, a movement's budget, the writing window's buttons: two rows under their note), margins are narrower (
Theme.gap). Checked by the window's tests at a phone's size (SIOUL_GRAB_PHONE, 412 × 891), which name every item past the right edge. Held sideways, a phone is often wider than 720 pixels: what a phone has or lacks is decided by the platform, never by the width; the status line's keys ("Ctrl+1 to Ctrl+0 places…",KeysHint.qml) show on a computer only (Theme.touch; the owner's new phone, 16 October 2026, showed them sideways:tests/qml/tst_keys.qml). - Accounts: "From this phone's accounts…" opens Android's chooser of the phone's accounts (Murena's, Nextcloud's, Google's…); the address chosen fills the form, Sioul finds the servers, and asks the password once, as on a computer. Mail, calendars and contacts then sync through Sioul's own code, not the phone's. Sioul writes nothing into Android's calendar, contacts or tasks providers: DAVx⁵, which syncs them on many phones, misbehaves when another app writes their rows (research/android-providers.md, §6).
- Joining your other devices on a new phone: nothing set up yet, no sync app needed (the owner's new phone, 16 October 2026: /e/OS's eDrive did not bring the Documents folder). The Porch's first card, "Join your other devices", and Settings ▸ Your folder and sharing offer Sign in to Murena: Murena's sign-in page opens in the browser, Sioul waits for it (twenty minutes at most; Cancel stops it), keeps the app password Murena gives it in Android's KeyStore, finds the sharing folder there (
Documents/Sioul), asks the passphrase once, and keeps its own copy of the folder in step with murena.io itself; a first exchange brings the settings, accounts and hours, and Sioul says which accounts still need their password on the phone (database.md). Another phone sharing too: asked once whether this one replaces it. "All files access" is not needed for any of it. - Sharing with your other devices: the sharing folder is the one your sync app carries (Murena's eDrive, Nextcloud's, Syncthing, FolderSync…), read by its path, with Android's "All files access" (asked from the sharing panel). Sioul asks no sync service to carry anything; where the folder lives on a Nextcloud one of your accounts reaches, it also reads the other devices' files there itself, a backup for a sync app late or stuck (database.md); or, no sync app installed, Sioul keeps the folder in step with it itself, in its own storage, where "All files access" is not needed for the sharing (database.md). Folders already shared by your other devices are offered, and folders are chosen in Sioul's own browser (
qml/FolderBrowser.qml, the folders read through that access): Android's picker refuses the phone's storage on /e/OS ("for privacy") and givescontent://addresses, read back as paths only for the phone's own storage (Theme.localPath). Murena's eDrive carries only fixed folders (Documents, Pictures, Music…, not the cloud's root): the folder goes inside Documents. Tried with eDrive 1.9.2: the folder came down at its next full scan. eDrive never passes a deletion on, and sends again each file Sioul sent, the same bytes, which gives it a new ETag on the server: where Sioul sends this phone's files itself, it takes its past rounds and the sealed files it swept out of the server, looking again when the server refuses a delete as changed there (412: still of the size sent, a sealed file still dated as sent, it goes), and at each listing, every six hours, its past rounds still there (database.md, "Taken out there"). Until 15 October 2026 those stayed on the server for good: the phone's past full rounds, 47 MB. A phone dead for good never takes its own out again: once it no longer counts (forgotten, or replaced by the new phone), each device that counts takes its past rounds below its last full round out of its folder, and its sending out of the server at a listing (database.md, "The log"). The phone itself keeps every sealed file eDrive brought. The notes folder stays on the phone (~/Notes, in the app's storage): its projects, budgets and bank movements come through the sharing once "Projects, budgets and the bank travel here too, sealed" is ticked, on any device: the setting travels to the others (docs/database.md). - No badge on Sioul's icon: every notification channel is made by
Channels.quiet(android/package/src/com/aurelienpierre/sioul/Channels.java), its badge off (setShowBadge(false)), so that a launcher shows no dot and Samsung's no count while a notification waits; no other place makes a channel, whichandroid/jvm-checks/ChannelsCheck.javachecks. Android keeps a channel's badge as the channel was first made and takes no change of it later: on a phone where an older Sioul made a channel with its badge, it stays until switched off in Android's settings (Apps ▸ Sioul ▸ Notifications, each category, "Notification dot", or the app's own switch). Moving each channel to a new id would turn the dot off by itself, but would also reset the sound and importance you chose for it (the doses' too); it is not done. - Passwords of accounts from another device never travel: each account asks for its own (Password… on its card), typed or taken from Bitwarden by Sioul's own client (
qml/AccountPassword.qml,qml/VaultUnlock.qml). No security key there: Sioul asks one through Qt WebEngine, which Android lacks. - Your OpenPGP security key (19 October 2026; NFC and USB-C 23 October 2026): the phone knows its certificate from the computer that read it (client.md, "Your keys on a security key":
data/pgp/cards/, public parts only), so it encrypts what it sends to you as well as to the recipients and hands your key by Autocrypt. With NFC, the key held against the back of the phone opens, decrypts and signs (client.md, "On a phone, held against it"): the PIN typed in Sioul's band first, never kept; then one tap that reads the key, checks it, gives the PIN and does the work, the screen kept from captures meanwhile. Read once on the phone (Accounts ▸ Encryption, Use a security key), it signs there too; until then Sign stays off with why ("read it once on this phone"). Reader mode is on only while Sioul asks for the key and is in front (SecurityKeyLink.java), so OpenKeychain, Yubico Authenticator and Yubico OTP keep working; NFC is not required to install Sioul. With neither NFC nor a USB host, the phone says "open it on a computer for now" for mail waiting for the key, with no button, and keeps Sign off. Plugged into the USB-C port, the key works the same way, its touch asked as on a computer; Android asks once, each time it is plugged in, whether Sioul may use it, and only its smart card interface is claimed, so Yubico OTP keeps typing its codes (one typed into the PIN's field is never sent as the PIN). A phone without NFC, or with NFC off, still takes a key plugged in.
Doses while Sioul is away¶
Android freezes an app it does not show: the window's minute cannot remind a dose. Each coming dose of the next two days is given ahead to Android's alarm clock (crates/sioul-app/src/alarms.rs calls sioul_android_set_alarms; android/package/src/com/aurelienpierre/sioul/DoseAlarms.java). At its time, DoseReceiver asks Rust what to say (sioul_alarm_decide), which first reads what your other devices marked (remote::alarm_news). Where the folder is found on the server, Sioul's own pull is trusted alone only once a pull begun since the alarm went through: this one, or another's that ran meanwhile, waited for, never passed over (two doses due at once, the window's minute pull). Begun since is judged by the process's steady clock for its own pulls, and for another process's by its time within the alarm's start and now: a clock set back never makes an older pull count. A pull that fails, or has not gone through within five seconds: the sync app is asked as before and waited for, its twenty seconds ended as soon as a pull since the alarm goes through, twenty-five seconds in all at most. No pull through since the alarm: each device whose entry or claim on the doses says nothing more since (by its own times), known closed only by what was read before, is said in doubt, whatever else the sync app brought (health.md, "No news at the alarm"). Without the folder found on a server, the sync app's twenty seconds as before. Taken there, nothing; not known, the alarm asks again as soon as it may be known (a device closed before the dose, once the news of its openings, as measured, and two minutes more for the clocks had the time to come: health::known_from), a minute apart at least, within its ten minutes' wait for news, then the doubt said first (health.md); in doubt only because no news came since it began, nothing tells when news comes: it asks again at the end of that wait (until 10 October 2026, each minute, a pull each time: health::tests::an_alarm_in_doubt_asks_again_when_the_doubt_may_end). The alarm never decides on what was pulled before it: on its pull it knows what it knew with the sync app asked, a second alarm knows what the first one's pull brought, and a pull that fails or is too slow leaves it to the sync app, as before Sioul's own pull (remote::tests::a_doses_alarm_on_its_pull_knows_what_it_knew_with_the_sync_app_asked, two_doses_at_once_the_second_alarm_waits_for_the_first_ones_pull, an_alarm_while_the_windows_pull_runs_pulls_again_after_it, a_doses_alarm_whose_pull_fails_asks_the_sync_app_and_never_knows_wrongly, an_alarm_whose_pull_does_not_go_through_in_time_asks_the_sync_app, an_alarm_never_takes_a_pull_from_before_the_clock_was_set_back, a_clock_set_back_never_hides_the_alarms_own_pull, an_alarm_that_reads_no_news_says_the_doubt_and_one_that_pulls_does_not, an_alarm_doubts_each_device_whose_own_entry_did_not_come). The reminder says it as Rust does, with Taken (sioul_alarm_taken: a quiet "Taken at 08:03" for half a day; more than half an hour late, a tap opens Sioul to say when, Android letting a button open no window). A tap on the reminder opens Sioul on the dose's question (DoseOpener, then sioul_android_take_opened, read by Rust when the window comes up). Sioul not on the screen, each of them is a session of its own for your other devices (database.md, "Devices"): up, what it records (the reminder, the dose's record, the answer), its export, down, then this phone's files sent to the server (Sioul's own send, where it sends there too) and the sync app asked to look unless Sioul's own pull works, so that the lowered entry is the one that goes up. Put away, the phone says it closed, after its last export: your computers know it holds no answer they have not read.
- Sioul not running: Android starts it for the alarm alone, without the window.
DoseAlarmsloads Sioul's library without Qt's Java side, which only Qt's loader starts, when the window opens: the library has its ownJNI_OnLoad, else Android would run the first among Qt's libraries (Qt Multimedia's).main.cppthen does whatmain()does before the window (once per process), and all an alarm reaches there speaks to Java directly, never through Qt's. - Time: Android gives an alarm's broadcast a minute (it comes from the background), the answer takes some twenty seconds (a few, on the pull alone); it is asked on a thread of its own, the broadcast held open (
goAsync); Taken's, sent from the foreground, gets ten seconds, let go at eight. Past 45 seconds the broadcast is let go, before Android takes Sioul for hung, and the reminder says to check in Sioul (Rust's own words when it fails: never silence, never a claim); Rust's answer, if it comes, replaces it. A JobScheduler job would get longer, but waits for its turn, its quota and the phone's sleep. - On time:
setExactAndAllowWhileIdle, which rings while the phone sleeps (Doze), with "Alarms & reminders", which Android 12 gives at install and lets you take back (Settings, Apps, Special app access), and Android 13 gives for good (USE_EXACT_ALARM). Without it, up to an hour late (setAndAllowWhileIdle);sioul_android_exact_alarms()tells Rust. Not Android's alarm clock (setAlarmClock): it would show an alarm in the status bar for good, a dose always coming within two days, and put the doses before your own alarm on the lock screen. - Restarts: the list is kept in the app's private storage and given again after the phone restarts (once unlocked: Sioul's files cannot be read before, and a dose due meanwhile rings then), after an update, and when "Alarms & reminders" is given back. "Force stop" in Android's settings, or "Alarms & reminders" taken back, takes the alarms away until Sioul opens again.
- The reminders: the "Doses" channel, at Android's default importance (its sound, no banner), yours to change. From Android 13 they need your yes, asked the first time doses are given while Sioul is on the screen.
sioul_android_notifications_allowed()tells Rust whether a reminder can show;sioul_android_remove_reminder(key)takes one away once its dose is marked, here or elsewhere. - Tried on a Gigaset GS290 (/e/OS 4.0, Android 12), with test doses given fixed answers (driven by
adb, through a receiver since removed). Sioul killed, the alarm started it alone, on time, and Rust answered in 20 s; Pris turned the reminder into its line, without a sound; more than half an hour late, Rust's own answer turned it into "tap to say when", and the tap opened Sioul in the process the alarm had started, Qt starting there as always. An answer held past 45 s showed the reminder to check in Sioul, then Rust's in its place; a tap, Sioul on the screen or not running at all, handed the dose to Sioul as its window came up; an update gave the alarms again. A restart cannot be tried from a computer (Android alone sends "boot completed"): the same receiver gives the same list. With a real dose (5 October, 23:00), the desktop off for hours: the alarm waited for news, then reminded at 23:10.
The time running¶
While a focus session runs, Android's notifications show it, ongoing: its task, a chronometer counting from its start, the time paused left out (paused: "Paused, 12 min so far", no chronometer), Pause (Go on while paused) and Stop. Rust puts it up, changes it in place and takes it away whenever the session changes, here or on another device: after each reading of the tasks and at the window's minute (crates/sioul-app/src/timenote.rs calls sioul_android_time_note, empty to take it away; android/package/src/com/aurelienpierre/sioul/TimeNote.java). A tap opens Sioul, as by its icon.
- Its buttons reach Rust through
TimeReceiver, Sioul's window running or not:DoseAlarmsloads Sioul's library alone, as for a dose, andsioul_time_actionreads first what your other devices did, so that a button a moment behind undoes none of it (Pause pauses only what runs, Go on only what is paused); then it acts as the window's buttons do (with the window in the same process, through the window's own functions, and the window follows), sends the change at once, and puts the notification up as it follows. Every change of the notification goes through Rust, one at a time: two buttons pressed in a row show in their order. A button's broadcast gets ten seconds; let go at eight, Rust's note comes when it is done. - No foreground service: a notification the app posts stays while Sioul is away, frozen or stopped by Android, and its buttons start Sioul when they need it. A foreground service would keep Sioul running all day for a notification that needs nothing running, and from Android 14 its notification can be swiped away all the same. Swiped away, this one comes back at the next change of the session; "Force stop" takes it away.
- The channel: "Focus timer" ("Minuteur de concentration"), at low importance: in the shade and the status bar, without sound or banner. From Android 13 notifications need your yes, asked the first time one shows while Sioul is on the screen, as for the doses.
- From another device: the sharing brings
time/running.toml; the notification follows at the next exchange (each minute while Sioul is shown, every five while it is put away and Android lets it run, and first thing at a button), not while Android keeps Sioul frozen. - Not tried on a phone yet: built and checked on a computer (Rust's tests; the Java compiled against
android-36).
Waking¶
The alarm at waking (health.md, "The alarm at waking") rings from Android's alarm clock, not from Sioul's window. Rust gives the coming wakings of the next eight days (crates/sioul-app/src/wake.rs calls sioul_android_set_wake; android/package/src/com/aurelienpierre/sioul/WakeAlarms.java), as JSON {rings, looks, week, zone, words}, whenever they change: at the window's minute, when the night, a day's change or the mornings are saved, when the sharing brings them, when Sioul is put away. Apart from the doses: its own list, request codes, channel ("Waking") and receiver (WakeReceiver).
- Android's alarm clock (
setAlarmClock): exact, rings through Doze, shown in the status bar and on the lock screen; the next one only. Only with "Alarms & reminders" (Android 12 gives it at install and lets you take it back; 13 gives it for good,USE_EXACT_ALARM): without it, nothing is set, never an inexact alarm, and the setting says so, with a button to Android's page. Not Android's Clock app: its intents can neither change nor take back an alarm, and this one follows each day. - Asked again (
WakeReceiver; Rust'ssioul_wake_next, on a thread of its own, the broadcast held open): at each ring; as each night starts and ten minutes before each ring, the sync app given twenty seconds first (setExactAndAllowWhileIdle), since Android keeps Sioul frozen while another device changes the night; after a restart, an update, a change of time or of zone, "Alarms & reminders" given back. Rust answers in the phone's zone as Java gives it (Sioul's own may lag five minutes behind a change). Rust silent, or its file not readable ({"unknown": true}): the list kept goes on, then the usual week (week, Monday first), repeated, in the phone's zone. A list made in another zone is moved to the same times on the clock. - Before the first unlock: a phone restarted in the night cannot read Sioul's files until it is unlocked. The list is kept where it can (device-protected storage: times and words only), and the receiver, the ringer and the screen run then (
directBootAware): the alarm rings all the same, and Rust is asked once the phone is unlocked. A ring missed while the phone was off rings as it starts again, when no more than half an hour late. - The ring (
WakeRinger, a service in the foreground while it rings; from Android 14 of the type alarms keep ringing with,systemExempted, allowed to apps with exact alarms): Sioul plays the phone's alarm sound itself, as an alarm (USAGE_ALARM), looping, rising from −40 dB to full over 30 seconds, evenly in decibels. A channel's own sound would be at one volume, and an insistent notification stops sounding when the shade is opened. The alarm sound not playing, the notifications' sound, the ringtone, then beeps: never silent. Music pauses (audio focus). It rings until Stop or 10 min later; its notification swiped away is Stop. After 10 min later, an alarm clock ten minutes on, kept for a restart, and a quiet line with Stop. - The screen (
WakeScreen): the "Waking" channel's notification, at high importance, without a sound of its own, with a full-screen intent: over the lock screen (showWhenLocked,turnScreenOn), the time, Stop and 10 min later, nothing else; Back does not end it. From Android 14 Sioul may be refused the full screen (canUseFullScreenIntent): it rings all the same, its notification comes as a banner, and the setting says the screen may not light up, with a button. Sioul's notifications turned off: the ring has nowhere to show Stop; the setting says so, with a button, and Sioul asks for them (Android 13) the first time a list that rings is given while it is on the screen. - Do-not-disturb: Android's lets alarms through by default (not in total silence). Sioul's own sleep never holds it: Java rings without asking, and the moment it rings, the night's end, is no longer sleep.
- Words: given with the list, in the person's language, since none of Sioul's files may be readable when it rings; a few kept in Java for before the first list.
- Try the alarm (the setting, on a phone): Rust hands
{at, words}apart from the list (sioul_android_wake_try,WakeAlarms.tryNow), and the alarm rings ten seconds on through the same path:setAlarmClock,WakeReceiver,WakeRinger,WakeScreen. Its own action (WAKE_TRY, apart fromWAKE_RINGinadb shell dumpsys alarm), request codes and kept keys: nothing written of the night, the next waking and its "10 min later" left as they are; not given again after a restart. Its 10 min later rings it once more, as a try, with Stop alone. Exact alarms, notifications or the full screen refused: nothing is set, and the setting says why, with Android's page. - Not tried on a phone yet: built and checked on a computer (Rust's tests; the Java compiled against
android-36).
Pauses¶
During a pause, "Paused" or "Free time" (pauses.md), the phone is silenced by a do-not-disturb mode of Sioul's own, one per pause: "Pause" (« En pause ») and "Free time" (« Temps libre »); a third, "Do not disturb (Sioul)" (« Ne pas déranger (Sioul) »), is Sioul's own do-not-disturb on every device (its switch, the focus timer, sleep), with the same exceptions and the same code. Rust asks (crates/sioul-app/src/dnd.rs calls sioul_android_dnd), Java does it (android/package/src/com/aurelienpierre/sioul/PauseMode.java, an AutomaticZenRule switched with setAutomaticZenRuleState).
- Its access: Android's "Do Not Disturb access" ("Modes access" from Android 15), which you turn on in Android's settings (Special app access); the manifest's line alone gives nothing. The pause's settings say whether it is given, with a button to that page. Without it nothing is silenced, and the pause's line says so. Taking it back removes Sioul's modes.
- Who gets through: what the matrix of what reaches you lets through then (attention.md, §3.3), as far as Android can say it. Rust works it out per mode (
Attention::silenceat the mode's column: Pause, Free time with its "Nothing at all", and for the third the strictest of Sleep and Do not disturb that hold;everywhere.rs,asks) and sends it beside the olderpeopleanddoses(dnd.rs, Ask:calls,messages,repeat,conversations,alarms,doses,events);PauseMode.Throughmakes the mode'sZenPolicyof it. An older Rust'speoplealone still reads as before: starred contacts and repeat callers, or nobody.- Calls: from nobody, starred contacts, contacts or anyone (
PEOPLE_TYPE_*). While this phone screens calls (Calls), every contact's call rings through the mode when the matrix lets anyone known or Always through call then, the screening having declined the others before the phone rang; anyone's when it lets strangers or hidden numbers through. Without the screening, starred contacts only, Android's nearest to Always through. - Messages: starred contacts when the matrix lets anyone write then, else nobody; never "contacts" nor "anyone" for the notification listener's sake. Android plays a notification's sound before any listener hears of it (
NotificationManagerService: the sound, then the listeners): a contact's message let through the mode, then held by Sioul, would ring once in a pause (a message-like notification from an automaton too, under "anyone"). The mode is what keeps a held message silent; calls differ, the screening answering before the phone rings (everywhere.rs,phone_does). - A second call from the same number within 15 minutes, whenever calls get through (
allowRepeatCallers). - Priority conversations (
allowConversations(CONVERSATION_SENDERS_IMPORTANT), Android 11 and later), whenever the Always through row of Messages lets them come: the conversations set to Always through in Settings ▸ What reaches you ▸ Exceptions, once marked Priority in Android (Notifications from other apps). Android 10 has no conversations: they are messages there, and the line says so. Otherwise none (CONVERSATION_SENDERS_NONE). - Alarms always, the alarm at waking among them; the sound of what you play (music, a video). Other apps' reminders and events, and the system's sounds, never.
- Priority channels allowed (
allowPriorityChannels, Android 15 and later; before 15, Android lets a channel that passes through whatever a mode says): Sioul's channels that pass (below), and any you set to pass yourself. - Everything else waits in the shade and the status bar, without a sound, a banner, a light or a lit screen, and is there after. Every part of the mode is set, none left to your own do-not-disturb's settings.
- Calls: from nobody, starred contacts, contacts or anyone (
- Starred contacts, and contacts: Sioul never writes stars nor contacts (they are yours everywhere): the settings open the Contacts app's starred list. The people of the Always through list who are not starred on this phone, or in no contact of it, are named in Settings ▸ This phone, each with their contact to open or Android's form to add one (do-not-disturb.md): while the phone screens calls a contact will do for their calls; their messages need the star.
- Emergency services' call back: they may call from a number you do not know (in France, 0 800 112 112 since October 2024). No exception for them was found in Android's call filtering: repeat callers let the second call through within 15 minutes, and starring that number lets the first through (adding it to your contacts too, while the phone screens calls). The settings say it, with your country's number where one is known (
crates/sioul-core/data/crisis-lines.toml, kindcallback, checked with the other numbers before each release). - Channels that pass: what a mode lets through of Sioul's own goes to a channel of its own made to pass do-not-disturb (
setBypassDnd), at Android's default importance (its sound, no banner), which Android allows with the access and only while you have not changed that channel (your change stays); the usual channels are never changed, so that outside Sioul's modes your own do-not-disturb holds them as before. Java picks the one that passes only while every one of Sioul's modes on lets it through (PauseMode.passesNow, from what each mode was asked):- The doses: "Doses during a pause" (« Prises pendant une pause »), made as a mode that lets them through starts; the usual "Doses" otherwise. In a mode that holds them, they wait in the shade, silent.
- An event's alarms: "An event's alarms during a pause" (« Alarmes des événements pendant une pause »), made as a mode that lets them through starts (Rust's
events, from the matrix's row for an event's alarms: fixed at once in a pause and under do-not-disturb); the usual "An event's alarms" otherwise (Events). Sioul's reminders before an event never go there: they stay in the quiet "Events". - New mail from someone Always through: "New mail from people always let through" (« Courrier des personnes qui passent toujours »), made the first time such a batch is told while one of Sioul's modes is on (Rust's
through, the matrix'spierce: told through the system's do-not-disturb, as critical urgency does on a computer); the quiet "New mail" otherwise (New mail). From the background service's process too: Java reads which modes are on there (MODE_MULTI_PROCESS). - The line says whether each passes, as Android has its channel then ("Dose reminders are silenced too: their channel was changed in Android's settings.").
- Yours and Sioul's follow each other (do-not-disturb.md): the end of a pause turns off its mode alone; Android combines the modes on, the strictest winning, so yours, if stricter, decides while it lasts. The phone's own do-not-disturb turned on or off (the quick settings, a schedule, Bedtime, another app) presses Sioul's switch on or off on every device, heard as it happens, Sioul closed or not: by
DndReceiver(Android's broadcast), and by the notification listener while it is bound (AppNotes.onInterruptionFilterChanged, forwarded toDndReceiver), whichever comes first; Sioul's going off turns the phone's own off too up to Android 14 (setInterruptionFilter(ALL), every other mode on then snoozed with it), and from Android 15 says it stays on, with Android's page. A mode turned off by you while Sioul could not hear it stays off: Sioul says so ("you turned it off here"), and turns it on again at its next "on", or at Silence this device again. - What the line says is read back from Android (
ZenPolicy's getters;answers::enteredindnd.rs): "Your phone is silenced, except calls from your contacts, messages from starred contacts, repeat callers and priority conversations. Alarms and dose reminders still come. Your events' alarms ring too." From Android 15 the modes are yours to change in Settings ▸ Modes, where your changes win; the line then says what the mode lets through as it is set there, and that Android's settings won, or, a part Sioul never sets, that the mode is silenced as it is set there. -
Which Android: 10 and later (a mode with its own rules and state); on 9 the line says the phone is too old, and nothing is silenced. Up to Android 14 the modes show among do-not-disturb's schedules; from 15 among the modes, with their icon and "When you press Pause in Sioul". Their settings in Android open Sioul. Per version:
Android Calls, messages, repeat callers, alarms Priority conversations Sioul's channels that pass 9 nothing silenced – – 10 as the matrix says none (no conversations: a conversation is a message, from a starred contact) pass whatever the mode says 11 to 14 as the matrix says as the matrix says pass whatever the mode says 15 and later as the matrix says, or as you changed the mode as the matrix says, or as you changed it pass while the mode allows priority channels (Sioul's do) -
The tile and the shortcut: "Pause" among the quick settings (
PauseTile), lit while the pause is on, and as a shortcut, at a long press on Sioul's icon or as an icon of its own on the home screen (res/xml/sioul_shortcuts.xml). Both open Sioul throughPauseOpener, which keeps the press for Rust (take_pause_pressed), five minutes at most: the pause starts, or shows when it is on already; neither ever ends it. On a locked phone the tile asks to unlock first: no pause from a pocket. Not exported: no other app can start a pause. - Not tried on a phone yet: built and checked on a computer (Rust's tests; the Java compiled against
android-36).
Sharing¶
Sioul takes what other apps share and the mail links they open, and your addresses are offered straight in the share sheet. Each becomes a draft, whose writing window opens: nothing is sent until you press Send (client.md).
- The door:
ShareActivity(android/package/src/com/aurelienpierre/sioul/ShareActivity.java), exported, takesSENDandSEND_MULTIPLEof any type (pictures, PDFs, text, any file) andSENDTOandVIEWofmailto:addresses, from a browser too (BROWSABLE). Not Sioul's window itself: a second Qt window would start in the sharing app's task, and another launch mode for the window would change how the doses' and the pause's openers reach it. - The request: it writes what came, whole, in Sioul's data folder (
handed/incoming/<id>.json: the address chosen, themailto:address,EXTRA_EMAIL,EXTRA_CC,EXTRA_BCC,EXTRA_SUBJECT,EXTRA_TEXT, how many files), brings Sioul's window up, and Rust makes the draft as the window starts or comes back (crates/sioul-app/src/outside.rs,sioul_core::handed), or at once when its library is loaded already (ShareActivity.nativeHanded,sioul_handed; never loaded for that, so as not to cross Qt's loader starting). - The files come as
content://addresses, which Sioul may read only while the activity that received them lives: Android takes its leave back with it. They are opened on a thread of their own (a cloud's app may fetch a file before it opens), the activity then ends (translucent, never shown, and notnoHistory, which would end it as Sioul's window comes up, before the files are open), and the copy goes on from the open files intohanded/files/<id>/, under the names their app gave, an extension added from the type when there is none (Sioul tells a recipient a file's type by its extension), each as.partuntil it is whole. Meanwhile the writing window says "Attaching 2 files…" and Send waits; once all are copied (<id>.done.json), Rust attaches them, on the window's thread, and the window says so, or which could not be and why. Sioul stopped meanwhile: its next start attaches those copied whole and names the others. - Sioul's copies go with the draft, sent or deleted, and each when taken off it; a folder nobody holds goes after a day. Requests and copies stay on the phone:
handed/is not among what the sharing carries (the drafts are, and their attachments are named by their place on the device that attached them, as on a computer). - Not taken: a file named by its place (
file://), and Sioul's own files (an address of its own providers): any app could otherwise have Sioul attach its private files. The writing window names them. At most 100 files at once. The clip an app joins to a text is not read either: it can hold the picture the share sheet shows, which is no attachment. - Your addresses in the share sheet (Android 10 and later): each account that can send is a sharing shortcut (
MailShortcuts.java) of the category the<share-target>ofres/xml/sioul_shortcuts.xmlnames. The share sheet shows its address under an envelope, with Sioul's quill on it; chosen, the draft is written from it (Android'sEXTRA_SHORTCUT_ID). The same shortcuts are at a long press on Sioul's icon, "Write from you@example.org": a new message from that address. - In step with the accounts: Rust gives the list at each start and when it changes (an account added, renamed, switched off or removed), only while the window is shown: Android limits how often an app in the background changes them. The most used first (the messages of its Sent folders on the phone), then by priority, then in your accounts' order; never more than Android takes (
getMaxShortcutCountPerActivity, less "Pause"). Java changes Sioul'smail:shortcuts only, never "Pause", and only when the list differs from the last one given. One you pinned to the home screen says, once its address is gone, that it is no longer in Sioul. - What they show: the address, and "Write from …" in your language. Nothing else of the account reaches Android.
mailto:, as RFC 6068 has it (sioul_core::mailto): the addresses,cc,bcc,subject,bodyandIn-Reply-To. Nothing else, above all noattach, which some programs take and which would let a web page name one of your files.- Which address writes: the one chosen in the share sheet; else Sioul's usual one, the first by priority among those that send, as for Write, and the writing window's From changes it when you have several. Sioul always has a usual one: it asks nothing first.
- No account that sends yet: the status line says so, and the copies go.
- Not Android's accounts (Settings ▸ Accounts): sharing needs none there. Listing Sioul's addresses among them takes an authenticator service, and a sync adapter to be of any use: a second place for your accounts, which Android, other apps (with the access) and the phone's backups see, for nothing the share sheet needs. Sioul's accounts stay in its own settings; the chooser of the phone's accounts only reads an address from them ("What differs on Android").
- Not tried on a phone yet: built and checked on a computer (Rust's tests; the Java compiled against
android-36).
Attachments¶
A mail attachment opens in the app you choose, and Save… puts it where you choose (client.md).
- No antivirus, and none claimed: a phone has none Sioul can call. The Reader says attachments are not checked there, and the line after each opening or save says it again; Sioul does not ask each time, as a computer without an antivirus does, since the answer could never change. Mail set aside keeps its attachments closed, and programs, scripts and Android's installers (
.apk,.apks,.apkm,.xapk) never open: they can be saved. - Opening: Rust writes the attachment in Sioul's cache (
<cache>/sioul/attachments/<message>/<name>), then asks Java (StepService.call, "attachment-open";Attachments.java):ACTION_VIEW, acontent://address of Qt's FileProvider (<package>.qtprovider, its root "attachments"), the type from the name, read only. Android's own "Open with" asks which app when none is set for the type. That app reads the one file and nothing else of Sioul's. No app for the type: the line says so. - The provider's folders:
res/xml/qtprovider_paths.xmlreplaces Qt's file of that name in the APK: Qt's four, for the files Sioul opens through Qt (papers, notes, letters), and the attachments' folder of the cache alone. Qt's ownopenUrluses the same provider, which is why there is one FileProvider only: with two, Qt would hand every file to the other one. - Saving: "attachment-save" starts
AttachmentSave, an activity never shown, which asksACTION_CREATE_DOCUMENT(Downloads, a folder, a cloud's app), copies the file there on a thread of its own, takes the cache's copy out, saved or not, and tells Rust (nativeSaved), which says it in the status line. The activity stays behind Sioul's window while it copies: Android's leave to write there goes with it. - The cache's copies of opened attachments go the next day, as on a computer.
- Checked on a computer:
android/jvm-checks/AttachmentsCheck.java(the address, the type, the grant, the question where to save, the provider's folders and the manifest) and Rust'sattachments::tests.
The card on the home screen¶
Three cards for the phone's home screen (widgets: a long press on the home screen, Widgets, Sioul), in Sioul's colours, light or dark as the phone is, rounded as the launcher's other cards. The full card, "Sioul", begins with today, then says what the Porch has for you, then what now is for. The mail and the agenda have cards of their own, "Sioul — Mail" and "Sioul — Agenda", which you place, size and order on the home screen as you like, or leave out. Rust writes what the three say, in one file (crates/sioul-app/src/homecard.rs); Java draws them (android/package/src/com/aurelienpierre/sioul/HomeCard.java, an AppWidgetProvider with RemoteViews: no QML in a widget): the full card in HomeCardToday.java, the lists' rows in HomeCardRows.java (the factory a RemoteViewsService gives, HomeCardService.java).
- Today, first (the full card): the date ("Thursday 8 October", « Jeudi 8 octobre »), the card's age beside it once it is old. Then the weather at the place chosen: now, its icon, its temperature and the chance of rain from 20 % (its words for screen readers); beside it, on the same row, the next four hours ("10:00", an icon, "16°", the chance of rain); under them the next morning, afternoon, evening and night, in order, each the next of its kind with two hours of forecast at least (its most marked weather, its lowest and highest, its highest chance of rain), named shortly since their order says the day; then the next seven days, each with its short name, its icon, its high and its low, its chance of rain; and, small, "Weather: Open-Meteo.com", as the forecast's licence asks (CC BY 4.0). The forecast is the window's (
backend::update_weather: fetched again when half an hour old while Sioul runs, kept in the state folder'sweather.json), hourly three days ahead and daily nine days ahead (weather::query), so that each hour the card is given ahead shows its own now, next hours and parts of the day (weather::card), and tomorrow's card still has seven days to come (weather::days). The card fetches nothing. No place chosen: one quiet line instead, "Choose a place for the weather in Sioul: tap the weather in its status line." A place but no forecast yet, or one more than a day old: "No forecast yet." The date and the weather show in every state, with the details off too: they name nothing and ask nothing. - Then what the Porch has for you, without the mail, one line each: today's doses due and not marked, as known (below); a code or sign-in link you asked a site for, as "Right now" says it, with its site, and under it its warning when the sender is not verified, until it expires; the reminders of dates asked, waits over, payments planned, papers to renew, contracts renewing and the money watch, from their time while they still make sense, three at most, soonest first ("Call the CAF · Asked for Friday 30 October", "Rent · €650 · Planned for Friday 9 October; the account will hold it."), as their row of the matrix of what reaches you lets them come (work's in work's times only; none asleep, in a pause or in Free time, as usual: attention.md), on a phone, which tells none of them as a notification yet; the calls declined, two at most, in the Porch's words, at the times their callers may reach you (porch.md, "Calls declined"); two events at once, the time to get there and back counted, today's until both are over and tomorrow's on its day; "Sioul is not set up yet." while no account is.
- Left out of the full card, and why: the mail (the Porch's lanes and its summary, project mail among them): the mail card lists it; the events: the agenda card lists them, and an event's reminders come as Android's notifications (Events); what your sites notified while you were away, and paper letters: mail by another way, on the Porch; the week's payments and whether the account holds them: the reminders say the payments due, Budgets the rest; the line on other apps' notifications held: their time comes by itself; the Porch's questions on your hours and your night: setting up, not now; the doses due while Sioul was closed: a question on the past, asked in the window, never a reminder.
- Then Now: the status line's sentence ("Work until 17:00.", "Meal until 13:00.", "Sleep: nothing disturbs until 07:00."); do-not-disturb on every device while it holds (below); the next step's title, in work or admin time only (why it comes now is on Now, a tap away); with it, where you stopped, the line you left, until you say it is done (tasks.md, "Where you stopped").
- Mail (the mail card), in a list that scrolls: the messages the Porch shows then, newest first, twenty at most: a thin bar in the account's colour (its place among your accounts, seven of
Theme.qml's chart colours, taken between their light and dark tones so that each reads on either ground), the sender and the subject in bold while the message is not read (its file without theSflag), when it came on the right ("14:05" today, "Yesterday", then "7 Oct", with its year when not this year's: each wording with until when it holds, so that Java renames them at midnight), and a line of its text. Never what the Porch folds (filed, less important), nor what is set aside, waits in the spam review queue or is hostile, even unfolded. Never a count, in figures or in words: the list says who wrote, not how many. Nothing to list: "Nothing waits on the Porch." - Agenda (the agenda card): the events of your calendars from today to a month on, sixty at most, those over and those cancelled left out, each under its day ("Today, Wed 7 Oct", "Tomorrow, Thu 8 Oct", "Mon 12 Oct": each name with until when it holds, so that Java renames the days at midnight), an event that runs under today. Each is a block in its calendar's colour (Sioul's green for a calendar without one), its text dark or white, whichever contrasts more (WCAG's ratio), with its title and its time: "11:00 – 12:00"; ending another day, "22:30 – Wed 21 Oct 06:45"; a whole day, its title alone; several days, "Until Fri 16 Oct". Read from the calendars once, then again when the window reads the agenda (
pim::show_pimcallshomecard::agenda_seen: a sync, an event made, moved or removed, an event removed and waiting for Undo left out) and when the day changes; the full card's two events at once come from the same reading. Nothing to list: "Nothing in your calendars for the coming month." No calendar: no Agenda. - Do not disturb, everywhere: when the do-not-disturb shared by your devices is switched on by hand or for a focus session, its line goes under the status line ("Do not disturb, on every device, until 15:00."), quietly, until it ends (a focus session's end is no promise: the part of the day under way only); not during sleep or a pause, whose own words say it. Its module hands each change to the card (
homecard::dnd_seen); without the window, the card's do-not-disturb lines alone are written again. - The Porch's hours hold on the mail card: each part of the day shows the mail the Porch lets through then (what the attention model shows then:
attention::Attention::mail, read byhomecard.rs; attention.md); outside the hours chosen for mail, what the Porch says then ("The Porch opens tomorrow at 09:00."), never mail waiting for later; after a pause, when it opens. "Open it anyway" is the window's, never the card's. - States: asleep (from winding down), today, the night's line, a dose due when doses remind during sleep (the default; held there by the matrix, attention.md, none) and a code you asked for; no reminder, no call, no step; the mail card says the night's line under its heading, the agenda card lists the events. In a pause, today and the pause's line alone; the mail and agenda cards say it under their headings. In Free time, today, its line, a code and a dose, which reach you, as it says; no reminder; no mail; the agenda. No account yet, "Sioul is not set up yet." on the full card and the mail card.
- Doses, as known (health.md, "Knowing"): never "missed", "late" or "not taken". A dose is said plainly while it is known not marked on another device, the Porch's rule given ahead (
health::known_until: a device closed cleanly is known until it starts again, one in use while its news stays fresh, five minutes); after that, or when it is not known from its time on, "…, 12:30: check before taking it." With one device alone, it is known until marked; marking it, in the window or with the reminder's Taken, writes the card again. - Details on the home screen (Settings ▸ Display, on a phone; on by default; this phone's own, never shared): unticked, the cards name no one and nothing. The full card says the date and the weather, the status line, "A code waits on the Porch.", "A reminder waits in Sioul." (once, however many), "A call was declined: it is on the Porch.", "Today, two events at once.", "Your next step" (its date or its length kept for Now, never its title nor a reason that names another step) and "Your line on where you stopped waits in Sioul."; no dose line (a medicine, even "a dose", says health). The mail card says only "Mail waits on the Porch." or "Nothing waits on the Porch."; the agenda card shows the events as "An event" at their times. For a phone whose home screen others see.
- Given ahead: Android freezes Sioul in the background, so the cards are written in frames, one per change of time until the end of tomorrow (work, admin, a meal, winding down, sleep, waking, the Porch's hours, free time ending, midnight), each with its words, its mail, its reminders and calls, and its step as they will be then (
quiet::modeat each change; Now as the plan picks it then); and the weather hour by hour, each hour with its own now, next hours and parts of the day. The messages, the events, the reminders and the calls' lines are written once (each frame names those it shows by their place), and every wording that changes at midnight carries until when it holds. Java shows the frame and the hour for now and draws again at the next change (a frame's end, an hour, a code expiring, a dose coming, going or no longer known, a reminder's time, an event over, midnight), with one alarm for every card that wakes nothing (exact when "Alarms & reminders" is allowed). - Written in Sioul's private storage (
files/state/sioul/home-card.json, version 3) when the Porch is computed (new mail among it), when the plan is made, when the agenda is read again, when a forecast is fetched or the place changes (homecard::weather_seen), when the reminders change (gathered each minute,remind::tickcallshomecard::reminders_seen), at the window's minute when a frame ended (midnight among them) or five minutes went by, and when Sioul is put away; drawn at once (an explicit broadcast to the cards,sioul_android_broadcast) when what they say changed. Outside the window, where Rust runs already: a dose's alarm and its Taken write the doses' lines. Older than a quarter of an hour, the full card says so in a corner, "at 14:05" ("yesterday at 14:05", then a date): mail, events, reminders and tasks come in only while Sioul runs. Past its last frame: "Open Sioul to bring this card up to date.", and the agenda card still. - Version 3: the frames are under "times", the full card's parts beside them ("today", "reminders", "calls", "overlaps", "stopped"). A Sioul of version 0.0.3 or older, installed again over this one, reads "frames", finds none, and says "Open Sioul to bring this card up to date." with its agenda until its own Sioul writes its card. This Java reads a card of version 2, written before Sioul was opened since the update: its lines, without today, until Sioul writes the new one.
- Nothing runs for it in the background: no fetch from the cards, no timer of Android's (
updatePeriodMillis0), no Rust woken on a schedule. The next step and the Porch need the tasks and the mail read and planned, the window's work (seconds on the 2019 phone); the time of day and the weather's hours, the parts soonest wrong, cost nothing to give ahead. - Taps: the full card, its date and its weather open Sioul; the dose, the code, a call and two events at once, the Porch; a reminder, what it is about (its task, its budget, its paper, its contract, as a reminder's notification on a computer opens it: "reminder" and its address,
homecard::reminder_opens); the step, Tasks ▸ Now, and that step's details when Now shows another (the card was older than the plan); where you stopped, Now. In the lists, "Mail" and its sentences, the Porch; a message, the Porch with it open in the Reader (PorchPage.openMessage; in Mail, by its file, when the Porch no longer shows it: filed, moved); "Agenda" and its days, the Agenda; an event, the Agenda on it (agendaPage.openEvent). ThroughHomeCardOpener, which keeps the tap in Sioul's state folder for Rust (homecard::opened, when the window comes up: main.qml'sopenThing, "card-mail", "event", "agenda", "task", "budget", "paper", "contract"), as the doses' and the pause's openers do. The rows fill their address into one template (setPendingIntentTemplate, mutable from Android 12 where it must be said; its class fixed, a row can only say where Sioul opens). No other button: no Done from the home screen, too easy to press by mistake. - Size: the full card about four cells by four, resizable. As it shrinks, its parts are left out, the least needed first, today kept longest: where you stopped, the step, two events at once, the calls, the reminders, do-not-disturb, the code and its warning, the dose and "not set up yet", then the seven days, then the parts of the day; the date, the weather now and the status line stay. When the four hours do not fit beside the weather now, the farthest are left out first, never wrapped onto a second row (all four from about 320 dp wide); fewer than seven days show below about 284 dp (four at least). The mail and agenda cards are about four cells by three; their lists take the card and scroll.
- The weather's icons are the window's (Breeze's, LGPL-3.0,
crates/sioul-app/icons/), made into Android's own vector drawings bytools/make-weather-drawables.py(res/drawable/sioul_weather_*.xml), white, tinted by the layout in its text colour, light or dark as the phone is. - On a computer nothing is written, unless
SIOUL_HOME_CARDis set: then~/.local/state/sioul/home-card.jsonshows what a phone's cards would say. - Their words are Rust's, in the person's language, the lists' headings ("Mail", "Agenda") among them, but for two of Java's ("Open Sioul to fill this card.", before Sioul wrote one, picked by the phone's language) and the names and lines the launcher's list of widgets shows, Android's strings in English and French (
res/values-fr), picked by the phone's language. - Colours in a list: the bars and the blocks are coloured by Java (a colour filter on a white drawable, which
RemoteViewsallows on every Android Sioul runs on), in tones fixed there; every text colour is the layout's, light or dark as the phone is. - Checked: Rust's tests (
cargo test -p sioul-app homecard: the frames, the weather at each hour, no place, the Porch's lines and no mail on the full card, version 3;cargo test -p sioul-core weatheron an answer of Open-Meteo kept incrates/sioul-core/tests/fixtures/weather/); the resources and the manifest linked byaapt2; the Java compiled againstandroid-36, its pure part run on a JVM (android/jvm-checks/CardCheck.java: what the full card says at a time, when it draws again, a card of version 2, an older Java meeting version 3; withSIOUL_CARD_SAMPLE, the cardthe_file_java_readswrites there). On a phone (8 October 2026, Android 12, in its dark theme): installed over the earlier Sioul, the card drawn by the launcher and looked at by its owner; the four hours beside the weather now come from that look. Not tried yet: the light theme, the card at sizes other than the owner's, a tap on each line.
Events¶
Reminders before events come on the phone as on a computer (reminders.md, "Before an event"): before each event (a quarter of an hour unless set, before its time to get there), the working day before, and at the alarms it carries, Sioul open or not. Android freezes an app it does not show, so the coming ones of the next eight days (a hundred at most, nearest first; one held while you sleep at your waking) are given ahead to Android's alarm clock (crates/sioul-app/src/eventalarms.rs calls sioul_android_set_event_alarms; android/package/src/com/aurelienpierre/sioul/EventAlarms.java), whenever they change: at the window's minute, as Sioul is put away, after each ring. Apart from the doses: its own list, alarms, channel and receiver.
- At its time
EventReceiverasks Rust what to say (sioul_event_decide, the library loaded without the window as for a dose, on a thread of its own, the broadcast held open): the event read again from its file (moved, cancelled, told already by the window?), sleep or a pause holding it or not (reminders.md: an event in the night is reminded in it, one after waking waits for waking, and its alarm is asked again at waking), the time left said as it is then. Rust shows it and marks it told (the window's minute and the alarm never both tell it). Rust silent past 45 seconds: the words given with the list are shown instead, never nothing; Rust's answer, if it comes, replaces or takes them away. - On time:
setExactAndAllowWhileIdlewith "Alarms & reminders", elsesetAndAllowWhileIdle, up to an hour late; Settings ▸ Reminders ▸ Before an event says which on this phone, and Settings ▸ This phone opens Android's page. Not Android's alarm clock: no alarm icon for an appointment. - Given again: after a restart (once unlocked: the events are in Sioul's files), an update, a change of time or of zone, "Alarms & reminders" allowed again: the list kept, at once, then Rust asked for the list as it is now, in the phone's zone (
sioul_event_coming); and once a day, nothing having rung. Events change on the phone only while Sioul runs (it syncs the calendars itself): the list is as fresh as Sioul's last run. -
The reminders, in two channels by what they are (Rust's
kindwith each, else the key:alarm:…):- Sioul's reminder before an event, and the working day before: the "Events" channel (« Événements »), at low importance: no sound, no banner, in the shade and the status bar, yours to change in Android's settings; never an alarm's category nor a channel that passes do-not-disturb, so a mode of Sioul's keeps them silent in the shade.
- An alarm the event carries (its VALARM, set by you or by whoever invited you): "An event's alarms" (« Alarmes des événements »), at Android's default importance: its sound, no banner, yours to change. While one of Sioul's modes is on and lets an event's alarms through (the matrix's row for them, fixed at once in a pause and under do-not-disturb; when its event falls then, in sleep and Free time, unless you change it), its twin that passes do-not-disturb, "An event's alarms during a pause" (Pauses, Channels that pass): the alarm rings through the mode, as the matrix's lock promises.
"14:00 · Dentist, in 15 minutes", the place below, Open; a tap, or Open, brings Sioul up on the event (
ReminderOpener, thensioul_android_take_reminder_opened, read by Rust as the window comes up). Gone from the shade an hour after the event began. From Android 13 they need your yes, asked the first time a list that holds one is given while Sioul is on the screen, as for the doses. -
Your phone's calendar app (through DAVx⁵, say) rings the alarms an event carries too, and so does Sioul now; Sioul's own reminder before is Sioul's alone (
X-SIOUL-REMIND, never a VALARM), so it is never doubled there. Turn off one or the other's notifications to hear an event's own alarms once: the calendar app's rather than Sioul's, since Sioul's modes let only Sioul's through (their policy lets no other app's events,allowEvents(false), so that none rings twice). - Each device tells its own: the computer's notification and the phone's. Not the device used last, as for new mail: an appointment missed costs more than a reminder twice.
- Not tried on a phone yet: built and checked on a computer (Rust's tests; the Java compiled in the APK,
android-36; the channels' choice checked on the JVM,android/jvm-checks/ModeCheck.java).
New mail¶
Told as on a computer (porch.md, "Notifications"): one notification per batch at the times your lists let mail through, "The Porch opens: …" for what waited; never during sleep or a pause. In Android's notifications (crates/sioul-app/src/eventalarms.rs calls sioul_android_mail_note; MailNotes.java): the "New mail" channel (« Nouveau courrier »), at low importance (no sound, no banner, yours to change), one notification at a time, the next batch in its place, gone after half a day; on the lock screen the count alone, never who; a tap, or Open, brings Sioul up on the Porch (ReminderOpener).
- Someone Always through in the batch (Rust's
through: told through the system's do-not-disturb, as critical urgency does on a computer) while one of Sioul's modes silences the phone: the batch goes to "New mail from people always let through" (« Courrier des personnes qui passent toujours »), at Android's default importance, made then to pass do-not-disturb (Pauses, Channels that pass). Outside Sioul's modes, the quiet "New mail" as any batch. - When Sioul fetches: on the screen, each inbox with IDLE, as on a computer; put away, the inbox's IDLE goes on until Android freezes Sioul (Android 14 and later: within seconds; earlier versions: until Doze or a kill). Mail that came while nothing fetched is told at the next fetch, if its time allows, else it waits for "The Porch opens".
crate::mailnote::arrivedandtickneed no window, for a fetch made in the background. - One device tells: the one you used last (the "notices" lease, porch.md); in the background the phone only reads the lease, never renews it, so a computer used later keeps telling. Two processes of Sioul's on the phone (the window's, and one fetching in the background) share the ledger under a lock, and each letter is claimed before it is told: never twice.
The end of the day¶
"Work hours are over" at the end of the day's last work or admin hours, and the night's own notice as the wind-down begins, as on a computer (reviews.md): the same times and once-only rules, from the device you are at, the same words. Android's notifications, in the "End of the day" channel (« Fin de journée »), at low importance (no sound, no banner, yours to change), made by Channels.quiet as every other; the end of work's with Close the work day, the night's with Close the whole day while that night's review is to give, and Options…. A press, or a tap on the notice (its first button), brings Sioul up on the sheet or on the night's question (ReminderOpener, then sioul_android_take_reminder_opened, as for an event), and takes the notice away; the press carries the day (work:2026-10-14), and once the day turned it opens nothing. The notice leaves the shade by itself six hours after it came, the end of work's at the next turn of the day (05:00) and the night's at waking if sooner.
- On time only with "Alarms & reminders": without it Android may ring these alarms up to an hour late (
setAndAllowWhileIdle), past the end of work's ten minutes and the night's quarter of an hour, and the two notices mostly do not come. Settings ▸ This phone says whether it is allowed and opens Android's page. - Their times go to Android's alarm clock with the events' reminders (
EventAlarms, kind "day":day:work:<date>for today and the next two days,day:night:<date>for the nights ahead;reviews::coming_work,health::coming_night), given again as the events' are. A time just past is given as it was, so that the list stays the same from one minute to the next, and Android rings it at once. An alarm Java keeps already is never set again in the past at the time Rust said before: it rang, or rings at the time its answer gave it; only a time Rust moved earlier than it said, while it is not done, is set again, though past (the hours edited so that today's end was a few minutes ago, a hold that ended early), and rings at once (EventAlarms.merged, which keeps Rust's last time apart,given; until 15 October 2026 each list set it again, and a list given every second rang it every second); a held event's next time moves in steps of a quarter of an hour, never "a minute from now" (eventalarms::again_at), so the list is not given again every second either. - At the alarm Rust decides (
EventReceiver,sioul_event_decide, thenreviews::decide_workorhealth::decide_night), Sioul's window running or not, as a computer's minute decides: the matrix of what reaches you, no meeting, the day not closed. Who keeps the notices ("notices" and "health" leases) is read, never claimed, from there (share::looked), as new mail does in the background. Early by a moment, or the end of work held by the matrix (a pause, do-not-disturb) while its ten minutes last, it is asked again a minute on; once it cannot come that day (the day closed, its review given, the evening's offer instead), it is told, never asked again that day. Rust shows the notice (sioul_android_day_note,DayNotes.java), or nothing: no words are kept to say in its place when Rust does not answer, as there are for an event, since the notice may rightly not come (EventAlarms.fallback). - Checked on a computer: Rust's tests, and
android/jvm-checks/DayNotesCheck.java(the buttons Java builds from Rust's words, which Rust's own test compares with what it says; what a press may open). Not tried on a phone yet.
In the background¶
"Sioul keeps your devices in step": with Sioul closed, a foreground service follows your other devices, so that do-not-disturb turned on or off on a computer reaches the phone within a few minutes, and new mail is told at its times (do-not-disturb.md). android/package/src/com/aurelienpierre/sioul/StepService.java, StepReceiver.java, DndReceiver.java; Rust's side crates/sioul-app/src/steps.rs.
- A process of its own (
:steps): Qt's activity ends its whole process when it closes (System.exit(0)inQtActivityBase.onDestroy, Qt 6.11.2), Back on the last page or a swipe from the recent apps among them; a service living there would end with it. The service's process holds Sioul's library without Qt's window (loaded as for a dose,DoseAlarms.load); the window's process stays as before, given back to Android when put away. - One quiet notification: the "Devices in step" channel (« Appareils à jour »), at the lowest importance: no sound, no icon in the status bar, folded at the bottom of the shade, hidden on the lock screen. It says the state now (
steps::note): its title, what now is for and until when ("Leisure until 22:00", « Loisirs jusqu’à 22:00 »; "Pause" while paused); its text, do-not-disturb (the status line's sentence while it holds, what the phone's own still holds while Sioul's is off, else "Do not disturb is off."), then the calls ("Calls are screened.", or "Every call rings until 15:30."), whole when opened. "Sioul keeps your devices in step" only until Rust first gave its words. A tap opens Sioul. From Android 13 it can be swiped away; the service goes on. - Up to date within a second: each apply of do-not-disturb in Sioul's own process tells the service its words once this device's table is written (
steps::tell_note,StepService.call"note", a broadcast toStepReceiverin the service's process, on Android's queue of foreground broadcasts, so that it never waits behind one held open, such asDndReceiver's while Sioul's own change settles): a press of the switch, the phone's own do-not-disturb heard, the tile, a mode's end at its alarm, another device's change brought by an exchange, the calls' switch, and the window's minute. The service shows them on the thread that hears them, never behind a step (which may wait for the server, the sync app or mail), and only when they changed; words made before those shown are left aside (at). Its own steps say them too, before mail, and the next step comes no later than the end of what now is for, so that the title changes with the time while Sioul is closed. - Its type:
specialUse, its reason written in the manifest. NotdataSync, which Android 15 stops after six hours a day; notremoteMessaging, which is for messages. - Started when Sioul's window comes up (Android lets an app start its service then), again at the phone's restart and after an update (
StepReceiver), and by its own alarm if Android stopped it (an exact alarm lets an app start its service from the background).START_STICKY. Off: Settings ▸ This phone ▸ Keep this phone in step in the background (this phone's own setting, never shared:[dnd] background); then changes come when Sioul opens. Sharing off, it stops. - A step (
steps::step), the phone kept awake while it runs (a partial wake lock, taken as the step starts, a minute and a half at most): at its alarm, where the folder lives on a Nextcloud and Sioul's own pull from it works (database.md: switched on, the folder confirmed there, a pull gone through and the last one too,remote::State::pulls_well), the pull alone, waited for twenty-five seconds at most (what it had before, beside the sync app's twenty), the step going on as soon as it is done, most often within a few seconds: the sync app is neither asked to look nor waited for, before or after a send. A dose's or a waking's alarm trusts its pull alone only once a pull begun since it went through, else asks the sync app as before (Doses while Sioul is away); the window on the screen no longer asks the sync app to look every five minutes (its minute's exchange pulls), nor when it comes back (an exchange pulls at once, once a minute at most). Otherwise (the pull switched off, not confirmed, never gone through, or its last one failed) as before: the sync app asked to look (eDrive's "force scan") and given twenty seconds, the server read meanwhile (Sioul keeping the folder itself: the server looked through at once, no sync app to wait for, and what the exchange wrote sent after it). Then a quick exchange (share::exchange_here, notes and papers left for the window); the sealed files of the others' notes and papers this phone waits for (an exchange of the window found their records and not them), fetched from the server by their names beside the mail, waited for twenty-five seconds at most (share::fetch_wanted_here, database.md, "Sealed files fetched when waited for"), so that the window writes those notes as soon as it exchanges; nothing fetched while nothing waits; the do-not-disturb this phone should ask of its system, compared with the last asked: changed, Sioul's own process applies it (DndReceiver, a broadcast to Sioul itself), where its modes are kept (PauseMode's memory); mail at its rhythm, begun with the step (below); then the next alarm. - The folder watched:
FileObserveron the sharing folder; another device's file written by the sync app brings a step three seconds later (by an alarm of its own, below; Android brings an alarm five seconds on at the soonest), the sync app neither asked again nor waited for. The step names the files written (folder:<names>), and Rust leaves it out when the pull brought each of them already, the same as fetched (steps::brought_already: whole for a small file, by its size for a round, which only grows), the alarm left as it was: a file the sync app carries after Sioul's own pull costs no exchange. Hidden names (files being written) and this phone's own files are left out (StepService.othersFile). - Awake only while it works: a step asked for later, a folder's (three seconds on) or a message's line for your computers and a new text (twenty seconds on,
StepService.SOON), comes by an exact alarm of its own (StepService.later, the actionLATER, through Doze: allow-while-idle), the phone left to sleep meanwhile; until 9 October 2026 the phone was held awake through those seconds. A step asked now takes the wake lock, and the alarm waiting goes: the step does its work. A message's step is never replaced by a folder's, which may be left out: a file written while a message's step waits brings that step at the folder's time instead, and a message while a folder's waits turns it into a message's (StepService.Later.of). - One burst of network a step: the pull, the sends after the exchange, and the second exchange's when the texts wrote something, go through one connection, kept by the process's one agent (
dav::shared_agent: ureq keeps an idle connection fifteen seconds; each request with its own limits,stalls::ask): one TLS handshake a step, where each pull, each send and each file sent opened its own before. Mail is fetched from the step's start, beside the pull and the exchange, every account at once; nothing in the step waits between them. On mobile data each wake of the radio keeps it at high power for some seconds after the last packet: one burst a step pays for one such tail. - Nothing new, nothing done: most steps find nothing new. The exchange first notes how what it reads stands (the shared files here, by size, time and inode; the other devices' records in the folder and in what the pull fetched, by size and time; what each device's entry says but for its exchanges' times: a computer in use restates its entry each minute), and when all of it is as the last exchange of the process left it, settled, it reads nothing and writes nothing, and says so as a whole exchange would (database.md, "An exchange with nothing new"). Then the hour's tidying, a change anywhere, or this phone's notes due again, and it runs whole, with the memory and the logs of lines kept in the service's process between exchanges (
share::set_keeping), the texts' log read through again only once it changed. - Nothing written when nothing changed: put away, the phone writes its entry into the folder only when what it says changes (its session, how far it wrote, its doses, its build), when the folder's copy is not the one it wrote, and once an hour (
devices::RESTATED); its notes to the others (<id>.toml) when how far it read changes, and once a day; the folder's leftovers of a crash are cleaned once a day, not after each change received. Each write into the shared folder costs Android's storage layer a few calls and a scan, eDrive an upload, and Sioul a send. - No full round while the texts are imported in bulk: the window's exchanges and the background step's say it before each exchange (
share::set_importing); the import's records go once, in the rounds that continue the last full one, and one full round follows it (database.md). - Its rhythm: two minutes while the phone keeps close (
steps::keeps_close): another device in use, its entry among the devices saying so and its last export fresh (devices::others_in_use; while you are awake, a computer whose Sioul runs, whoever is at it; while you sleep, a computer only with someone at it), or, while you sleep, a dose due and not marked while another device runs (below, "Doses while you sleep"); otherwise five while you are awake, fifteen while you sleep (the sync app not asked then, unless the phone keeps close; the pull at its own pace). A step reads the others' entries before its pull, for whether it pulls at once, and again after it, for when the next step comes: someone back at a computer is seen from the step that brings the news, not one slow step later (until the review of 13 October 2026, the second reading was missing, and at night the two-minute steps never came back). While Sioul's window is on the screen the steps wait: it exchanges by itself each minute. - Someone at a computer, while you sleep (since 13 October 2026): a computer whose Sioul runs says in its entry whether a person is at it (database.md,
active): input within ten minutes and its session unlocked, as its system says (KDE's and GNOME's screen savers and idle monitors, logind's hints; Windows' last input and its session's lock; macOS's last input and its screen's lock;sioul_sync::power::present_of), or a press in Sioul's window (its event filter,cpp/application.cpp) or of do-not-disturb's switch, which counts at once, whatever the session's last reading said of its lock when the press is newer than it (a reading is timed from when it began). A press in the window goes out with the next export, at the minute's cadence, never in an exchange of its own, which would tell the server when someone came back to the computer; a press of the switch goes out with the switch's own exchange, which says the press anyway. Lock answers that disagree (logind's hint left locked while the desktop's own screen saver says unlocked; GNOME's shield up with no lock behind it) are left aside: input alone says whether someone is there; no input said, unknown, Sioul running counts. The session, the power supply and the connection's cost are read on one thread of their own every thirty seconds (power::watch, throughdevices::watch_system), on one connection to each bus kept for the process and made again only once the bus dropped it: an exchange, a start or a close only reads the values kept (kept_session, andkept_powerfor the window'smetered), and never opens a bus nor asks it anything. A bus that stops answering once the thread connected only has its questions given up, a second each (power::tests::the_reading_keeps_its_connections_through_a_stopped_bus, with two stand-in buses). Every D-Bus connection of Sioul's, this one as the notifications', do-not-disturb's and colord's, is made bysioul_sync::bus: a plain connection, itsHellosaid by Sioul, the whole setup waited for five seconds at most on a thread of its own, never libdbus's own registration, which holds its process-wide locks across aHellowaited for without limit, so that a bus that takes a connection and never answers held every other D-Bus connection of the process and Qt's own at Sioul's exit. Such a bus now holds that one thread, and further connections to it are refused at once until it answers (bus::tests::a_bus_stuck_at_setup_holds_nothing_else;bus::tests::nothing_but_this_file_reaches_a_bus). The keyring is the exposure left: it reaches the Secret Service through thekeyringcrate, whosedbus-secret-serviceregisters through libdbus at each read. While the session bus stops answering, a keyring read (the backup's check at each exchange, a mail fetch, a CalDAV or CardDAV sync) waits until it answers, holding libdbus's process-wide locks meanwhile: the exchanges stop within a minute (the others then doubt this computer, the doses' safe side), connections of Sioul's that close or learn their bus is gone wait too, and Sioul cannot quit until the bus answers. It is within reach, by the keyring's own backend on zbus or a keyring call bounded asbusbounds its connections; that touches the vault's way to its secrets, and waits for the owner's decision (review of battery part C, M1). Qt's own D-Bus (the tray, the desktop's theme) registers so too, inside Qt. No reading kept, or one two minutes old, unknown. Only while you sleep does it matter: then a computer left on with nobody at it, its Sioul in the tray all night, no longer makes the phone step every two minutes, and the phone steps as with no computer running. While you are awake, a computer whose Sioul runs counts whoever is at it, as before, so that do-not-disturb from a computer keeps its two minutes all day (chosen on 13 October 2026, after a review: most of the saving is at night). A computer whose system says nothing of it, and an older Sioul's, count by Sioul running, as before. Do-not-disturb pressed at a computer reaches the phone within two minutes while you are awake. While you sleep, pressed at a computer nobody was at, it comes at the phone's next look, fifteen minutes away at most, and the step that brings it brings the two-minute steps with it (steps::tests::a_press_at_a_desk_brings_the_two_minute_steps_from_the_look_that_reads_it). - Doses while you sleep: while a dose is due and not marked (from its time until it is marked, here or in the records another device sent, or its day ends:
health::dose_open_now) and another device runs Sioul, the phone keeps its two-minute steps at night too, so that a mark made at a computer reaches it within them; the home screen's card says the dose plainly at most two minutes past such a mark, where the doses' freshness alone allows five (steps::tests::a_dose_due_keeps_the_phone_close_while_a_computer_runs; health.md). - Network wakes an hour, from the code (
steps::tests::the_phone_wakes_as_often_as_someone_may_be_at_a_computer, its steps reading the others' entries as the last pull left them; one burst a step: the pull, the send and mail together): no computer running, 12 awake and 4 asleep, as before; a computer left on with nobody at it, 30 day and night before, now 30 awake, as before, and 4 asleep; with a dose due and not marked, 30 asleep too; someone at a computer, 30, as before. Mail's rounds, four an hour while you are awake, fall in those bursts; none while you sleep, as the usual matrix says. A message for your computers or a text received adds one, unless a step comes within its twenty seconds. - Through Doze:
setExactAndAllowWhileIdle(with "Alarms & reminders", which Android 12 gives at install; elsesetAndAllowWhileIdle, which Android may defer); a process with a foreground service keeps the network and its wake locks during Doze. The sync app must be let work too when it is asked: in deep Doze eDrive's scan waits for Android's next maintenance window unless eDrive is exempted from battery optimisation (on /e/OS,adb shell dumpsys deviceidle whitelist | grep drivesays whether it is). Android allows an app some 72 allow-while-idle alarms an hour: the steps (thirty an hour at most) and those asked for later stay under it. - "Working": the service is no session: it never says the phone is in use. Its exchanges say how far the phone wrote and when it last shared (
devices::exported, insideshare::exchange_here), in the folder when how far it wrote changed and at least once an hour, so a computer's doses take the phone as closed and up to date (health.md, "Knowing"; database.md); and the phone's own knowledge of a computer in use stays fresh, at two minutes, while you are awake. While you sleep and nobody is at it, the phone reads it every fifteen minutes, unless a dose is due and not marked (above): between two looks, a dose due before reads as "check" on the home screen's card once its news is five minutes old, and a dose's alarm, which pulls first, reads the computer's entry fresh (written at each of its exports, each minute). - Mail: every fifteen minutes, never while you sleep or pause unless the Mail rows let new mail come then (attention.md): your Always through set to come at night keeps its rhythm, in the night's steps, and nothing else wakes for mail then (
steps::tests::mail_at_night_only_when_the_matrix_tells_some_then); only when new mail is told at all: begun with the step, beside the pull and the exchange; every account at once, each in a thread of its own, its inbox alone, in one connection, without IDLE (sioul_sync::inbox); what your own spam filter judges, of a class you chose "Move to spam" for, moved into the account's Junk folder (spam::after_fetch, spam-filter.md); its arrivals handed to the new-mail notifications (mailnote::arrived, thentick), which never tell what the filter flagged or moved; the batch told as soon as every account is fetched (mailnote::close_now), in one notification. Until 9 October 2026 the accounts came one after another, and the step stayed awake twelve seconds more for the batch's ten. One fetch per account at a time across processes (the account's lock): the window's watcher and the service never write a letter twice, and the ledger never tells one twice. - Battery optimisation: /e/OS may stop an app in the background to save the battery. Settings ▸ This phone says whether Sioul is exempted, with Allow in the background (Android's question,
REQUEST_IGNORE_BATTERY_OPTIMIZATIONS; else its list: Settings ▸ Apps ▸ Special app access ▸ Battery optimisation ▸ Sioul ▸ Don't optimise). - Its cost, measured on a phone (a Gigaset GS290, /e/OS 4, Android 12, about 3,000 mAh, on mobile data, eDrive carrying the folder, 8 and 9 October 2026, before the changes of 9 October): Sioul 4 % of the battery in 13.5 hours in the background, and Android's storage entry (its FUSE daemon, which serves the shared folder, and eDrive's listings of it) another 4 %; those hours included a texts' first import, whose full rounds were sent twice. A reading of the code on 9 October found, for a day with a computer's Sioul running: about 490 steps, each about 25 seconds awake, twenty of them waiting for eDrive (some three hours awake); eDrive's forced scans, 450 to 1,450 a day against its own 48, each listing every folder it carries (the sharing folder's thousands of files among them); two or three new TLS connections a step; mail's accounts one after another, then twelve seconds awake. Changed on 9 October, with what each saves by that reading's estimates, to be measured:
- no forced scan, no twenty seconds, while the pull works (above, "A step"): about 2.5 to 4 hours awake a day less, and eDrive back to its own pace: 5 to 9 % of the battery a day, the largest part on the storage line;
- awake only while working, mail's twelve seconds gone: 10 to 40 minutes awake a day, 0.2 to 0.6 %;
- one connection and one burst a step, mail's accounts at once: 0.1 to 0.4 %, more on mobile data, where each burst pays the radio's tail once;
- the texts' import only when the phone's texts changed, a heavy one only while it charges (texts.md): little a day (two queries to Android a step), and a bulk import's hundreds of megabytes sent on the charger, its full rounds held back meanwhile;
- a folder's step left out when the pull brought its files: a few minutes awake a day, and as many exchanges.
Changed on 10 October, measured on a computer with an invented folder the size of that phone's (
share_idle_bench.rs: 6,000 sealed files, afiles.jsonof 15 MB, a texts' log of 10 MB; on the phone's slower cores each figure is larger, in the same proportions): - an exchange with nothing new reads and writes nothing: 78 ms of CPU before, a third of a millisecond now; two hours of steps beside a computer in use, 3.4 ms each on average, the hour's whole exchanges among them. The research's estimate for the old work was 0.4 to 1.2 % of the battery a day;
- an exchange with something new: 64 ms where it was 82, the memory and the texts' log no longer read again, and the send's question (
own_sealed) 17 ms to nothing; - nothing written into the folder at those steps, where each wrote the entry (about 500 writes a day, each uploaded twice and fetched by the computers);
- the dose's and the waking's alarms on a pull begun since they went off: a few seconds awake rather than twenty-five, two to four times a day; the sync app's twenty seconds as before when that pull fails or is slow. Memory: Sioul's library and Qt's in a process without a window, about 30 to 50 MB (proportional set size), against about 140 MB for the window put away; with the texts shared, the memory and the texts' log kept between exchanges add about 30 MB there for 10 MB of texts. Measured on the night of 10 to 11 October, with those changes (0.0.5; 14 h 46 on the battery, 10 h 22 of them with the screen off, on mobile data): Sioul 119 mAh, 4 % of a 2,946 mAh battery, of which the mobile radio 91 mAh, the processor 16 and the wake lock 1.4; eDrive 37 mAh; the storage entry 9 mAh, from about 4 % before. The cost left is the radio: how often a step wakes the modem, not how much it sends. A computer left on all night counted as in use, so the phone stepped every two minutes. Changed on 13 October:
- while you sleep, in use means someone at the computer (above, "Someone at a computer, while you sleep"): a night with a computer left on, 30 wakes an hour down to 4, but while a dose is due and not marked; awake, 30 as before; the estimate in "Measuring it" below;
- a message received (a multimedia one too) brings its step at once, as a text did (below, "Texts").
Left for later, for you to choose: coalescing the two- and five-minute steps with Android's windows (
setWindow, a minute either way), which would let a computer's do-not-disturb come up to a minute later; taking the folder off shared storage (remote::MIRROR), a move of your data.
- Measuring it on a phone (read only;
--charged: since the last full charge, the phone unplugged meanwhile):adb shell dumpsys batterystats --charged com.aurelienpierre.sioul(the wake lock "sioul:steps", its hours and count; the alarmsSTEPandLATER; the CPU of:steps; the mobile radio's time and bytes), the same forfoundation.e.drive(its jobs: the scans) andcom.android.providers.media.module(the FUSE work);adb shell dumpsys batterystats --charged | grep -A400 'Estimated power use'(each uid's share, in mAh: wake lock, CPU, radio);adb shell dumpsys alarm(the wakeups by action, and the next step);adb shell dumpsys jobscheduler(eDrive's recent jobs);adb shell dumpsys meminfo com.aurelienpierre.sioul:steps(its PSS);adb logcat -s sioul(each step's pull and send: "backup: … pulled from …"; each step's exchange: "sharing: nothing new, nothing read", or what it sent and received). The same before and after an update, over the same hours, compares them. The estimate for 13 October's change, from the night of 10 to 11 October: some 440 steps in those 14 h 46 (one every two minutes), about 0.2 mAh of radio each; now 4 an hour while you sleep where there were 30: 208 steps fewer in an eight-hour night, about 40 mAh less, if the radio's share follows the wakes (the hours awake, mail, messages and texts unchanged; less on a night a dose stays due and not marked). To measure: a night with the computer left on. - Tried on a phone (8 October 2026, Android 12): the notification's words, its title changing with the time, and each change of do-not-disturb shown within a tenth of a second (do-not-disturb.md). Its cost is not measured yet.
Calls¶
Sioul screens the phone's calls as Android's "Caller ID & spam app" (« Appli numéro de l’appelant et spam », Android 10 and later): Android asks it about each call before the phone rings, and Sioul lets the call ring, or declines it plainly, as you would by hand, and the network sends it to your voicemail. When the people you know may call is the Calls rows of the one matrix (Settings ▸ What reaches you ▸ By person ▸ Calls, attention.md); what becomes of the numbers nobody knows is the call screening's, apart (below); the research behind it is research/call-screening.md. Java decides (android/package/src/com/aurelienpierre/sioul/CallScreen.java, Calls.java), from a table Rust wrote ahead (crates/sioul-core/src/calls.rs, crates/sioul-app/src/calls.rs).
- The role: Settings ▸ This phone, on a phone, says what Sioul does, what it never does and what always rings, then asks Android, "Set Sioul as your default caller ID & spam app?" (
RoleManager.ROLE_CALL_SCREENING, fromCallsSetup, an activity never seen: Android answers only an activity that asks for a result). It gives no other permission. To stop: Android's default apps ▸ Caller ID & spam app ▸ None (the page's button opens them). One app holds the role: a phone app's own spam filter gives way to it. Android 9 has no such role: the page says so, and do-not-disturb by time is what holds calls then (Pauses). - The contacts: with
READ_CONTACTS(asked already for do-not-disturb), Android asks Sioul about every call, your contacts' included (Android 11 and later; Android 10 asks about every call when the carrier configures no screening app of its own). Without it, Android rings your contacts' calls without asking, and Sioul screens the others; the page says so, with Android's question. - In milliseconds, without Qt: Android holds the ringing until it hears back, five seconds at most. The service lives in a process of its own (
:calls), never loads Sioul's library or Qt, and readsfiles/state/sioul/calls/table.json, parsed again only when the file changed. The table holds who each number is (your lists and address books, byphones::key: safe, neutral, restricted, blocked), the lists' prefixes (tel:+3346571*), the numbers that always ring, Expecting a call as your devices last said it, the call screening's settings and the numbers it knows by their kind (below), and the next four days in frames, each with whether each row of the people you know rings then and whether it is work or admin time (calls::frames). Rust writes it again when what it is made of changes, and at least every ten minutes while something of Sioul's runs: the window's minute and each do-not-disturb applied, the background service's step, Android's daily look for the events' reminders. Made from today's midnight, a table made a minute later says the same and is not written again. Sioul closed four days, and nothing of it running: the calls ring. So do they before the phone's first unlock after a restart: Android does not find the service then (it is not direct-boot aware). - The order (
Calls.decide, a table of version 2, as the review of 19 October 2026 left it). An outgoing call to an emergency number marks the moment; any other number you call is remembered while the call screening counts numbers called (called), 14 days at most (calls/called.json, by its key: this phone's own, never shared; pruned at each call and each table made, and gone, nothing recorded, while the setting is off). An incoming call: (1) what always rings: an emergency number (Android's own list for the SIM and the network,TelephonyManager.isEmergencyNumber, and every number ofcrates/sioul-core/data/crisis-lines.toml, the callback 0 800 112 112 among them), every call for 24 hours after you called one (and, from an older table, the people on the do-not-disturb list); (2) a blocked number: declined, however often it calls, on the Always through list or not ("blocked entirely": Expecting a call too keeps it out); (3) for a number nobody knows (on no list, in no address book, not called lately), what the call screening keeps out whatever else, while it screens and the table is current: a number never shown as a caller's (089), a failed verification, an automated caller, each while it goes to voicemail (below); no second call, no hours, no rule and no Expecting a call lets it ring; (4) a second call from the same number within 15 minutes rings, whoever calls but those (Sioul counts calls itself: Android's repeat callers never see a call it declined; hidden numbers cannot be told apart and are not counted), and but a number you called lately whose verification failed while the screening sends failed verifications to voicemail: a borrowed caller ID of a number you called, calling twice, is no second call (the re-check of part 2, R1;Calls.decideandcalls::decidealike); (5) the people you know: their number on your lists, else a prefix on them, else a contact of this phone only, which is neutral, else a number you called in the last 14 days, which counts as your contacts do: declined by their row, it rings only while you expect a call, and never when the network could not verify it (a borrowed caller ID of a number you called); Always through's people as "always-" and their own state: the frame for now, the caller's row ringing or declined; (6) everyone else, by the call screening (below), never where your contacts' row declines but while you expect a call. No table, a table that does not read, past its frames, a row the frame does not name, anything that fails: the call rings, as Android would. A table of version 1, written by an older Sioul until this one writes its own, is read in its own order: "Let every call through" after the blocked, then a second call, then the frame's row for everyone, strangers and hidden numbers included. - Declined plainly:
setDisallowCall,setRejectCall,setSkipNotification: the network takes the call as declined by hand (most operators send it to voicemail, as when busy), and no missed-call notification shows. It stays in the phone's call history, as blocked, with Sioul's name: Android writes it whatever an app asks. Never "silent" (shown without a ring), never answered, recorded or listened to. - The call screening, apart (
crates/sioul-core/src/screening.rs): what becomes of a call from a number nobody knows. The owner's decision of 14 October 2026: "call screening is not only distraction protection, it's spam protection too, so it needs to be separated from don't disturb mode and might get enabled 24/7"; its rules are his decisions 3 to 5 of 18 October 2026, and his decisions after the review of 19 October 2026 (F4, F7). Neither do-not-disturb, nor a focus session, nor any state of yours switches it off or opens it: the people you know are decided before it, by their rows; it decides the rest, in this order:- what it keeps out whatever else, for a number nobody knows, while it screens: a number that cannot be a caller's (
impossible), one the numbering plan forbids as a caller ID (089, the plan's § 2.4.5 e), whatever the network says; a failed verification (unverified: Android'sgetCallerNumberVerificationStatus,FAILED, which few networks say yet); an automated caller (automated): a number from the roots ARCEP keeps for automated systems since 1 January 2023 (verified multi-use numbers: 01 62, 01 63, 02 70, 02 71, 03 77, 03 78, 04 24, 04 25, 05 68, 05 69, 09 48 and 09 49, and their overseas twins). Each goes to voicemail at all hours, its second call within 15 minutes too, and while you expect a call; each may go "as other numbers nobody knows" instead. Texts from an automated caller's number nobody knows are gathered with the automatons on the phone (Notifications from other apps). A known number whose verification failed rings all the same when their row lets them ring, since the people you know are decided before the screening and declining one of them is the worse mistake; its line says the network could not verify it, and their sheet shows it. A number you called lately keeps that one check where only Expecting a call would let it ring; - Expecting a call (below): numbers nobody knows, hidden numbers and numbers you called lately ring while it lasts, even where your contacts wait; the kinds above stay out;
- off (Settings ▸ This phone ▸ Calls ▸ Call screening,
on): every number nobody knows rings where your contacts may, automated callers too; the blocked stay declined. Under Reachable your contacts may ring at every hour (the frames'reachablecolumn), so with the screening off such numbers then ring at every hour too: what "off" means (review of part 3, L5). Expecting a call pressed on a device gone from the sharing (forgotten, replaced, left) reads as "Screen calls again" at its stamp, in the table as in every window: it opens nothing, and ends what came before it (re-check, R-1); - its hours (
hours): at all hours, as usual, or only outside your work and admin hours, in which numbers nobody knows and hidden numbers ring where your contacts may (the frame'shours: work or admin time, whatever its layers; a pause, a meal, sleep or Free time is none); the kinds above stay screened at all hours; - its rule for a number in no address book, Sioul's or the phone's (
unknown), and for a hidden number (hidden): voicemail, ringing in work and admin hours (a hospital's switchboard calls then), or ringing, where your contacts may. - Never beyond your contacts: whatever the screening lets ring by itself (off, resting in your work and admin hours, its rule) goes to voicemail where your contacts' row declines then (
frame.ring["neutral"]): under do-not-disturb's switch or a focus session, at night, in a pause, in Free time; the call's line then says that numbers not in your contacts go to voicemail then. Only Expecting a call opens beyond. So the screening never opens what do-not-disturb holds ("a ringing phone is what do-not-disturb promises to stop", the proposal's 3.8; the review of part 2, F3). - Kept in
[screening]with your settings, each key unsaid at its first value: unknown and hidden numbers from your Calls rows of strangers and hidden numbers as they were before the screening was apart, the closest rule that never rings where the row declined (ringing at every hour only when the row rang at every time; ringing in work and admin hours when it rang in either; else voicemail); the rest voicemail, numbers called counting. Written a key at a time, only when you change one there; the first written also writes unknown and hidden numbers at their first values, which the Calls rows then move no more. No preset nor any write of the matrix touches those two rows (a write asking it is refused, said); an older Sioul keeps the section. The Calls rows of strangers and hidden numbers stay in the matrix for those first values and for when a declined call is listed, never shown in its grid. - The numbers it knows by their kind ship as data, each group with its source and the day it was read (
crates/sioul-core/data/screening-numbers.toml; research 3.2 and 3.3, CS12): ARCEP's plan of 1 January 2026 (decision n° 2025-2215) for the roots for machines and 089; ARCEP's list of attributed blocks (MAJNUM, read on 19 October 2026) for the blocks it had attributed to nobody then, whole blocks of 10,000 numbers or more, made bytools/screening-numbers.py. That list decides nothing: a block attributed since may hold a new line (the review measured, on older copies of the list the Internet Archive kept, about 25 to 30 blocks, 220,000 to 260,000 numbers, attributed into such gaps every 90 days). A number from it is screened as any number nobody knows, and a declined call's line adds what the list said, with its date, until 17 January 2027, 90 days after it was read; from that day the line no longer says it, and nothing else changes, until a later version reads the list again. Only 089 declines on its own, as the plan says it is never a caller ID. The public-interest numbers (01510, 02810…, the plan's § 2.3.10 a) are left as any number nobody knows (CS12), even where ARCEP has attributed no block of theirs yet. The fiction numbers of Sioul's tests (01 99 00, 02 61 91…) are among the blocks attributed to nobody: the tests make their own tables. - What a screened caller hears: Sioul declines the call, as you would by hand, and your voicemail answers at once (decision 5). Android also allows letting it ring out on the caller's side; whether that reaches your operator's voicemail needs a test call first.
- On every device, What reaches you's cards say that numbers nobody knows and hidden numbers are the call screening's, and, where your contacts wait (do-not-disturb's card, the night, a pause, Free time), that such calls go to voicemail then, unless you expect a call; a person's sheet says so of someone no list or address book knows. The phone's modes let anyone's call through when the screening lets such numbers ring then and your contacts may ring then (
attention::Phone::unknown,calls::unknown_rings): nothing it lets ring is silenced, and nothing more rings.
- what it keeps out whatever else, for a number nobody knows, while it screens: a number that cannot be a caller's (
- Do-not-disturb is apart: Android applies its modes after Sioul's answer. While one of Sioul's modes is on (a pause, Free time, the do-not-disturb switch, a focus session or sleep when they silence the phone), a call Sioul lets ring still rings only as the mode lets it: while this phone screens calls, every contact's (the screening declined the others already), anyone's when the call screening lets numbers nobody knows ring then and your contacts may ring then, or while you expect a call; a second call within 15 minutes (Pauses). Do-not-disturb never moves the call screening, nor opens it: where your contacts wait, numbers nobody knows wait too, but while you expect a call. Someone Always through who is in no contact of the phone is let ring by Sioul and silenced by the mode: Settings ▸ This phone names them, with Android's form to add them. So is a number you called lately that your contacts' row lets ring, unless the mode lets anyone's call through then. Your own do-not-disturb decides as you set it. The floors are the screening's own, not do-not-disturb's.
- The table and the matrix: the frames carry the matrix's Calls rows of the people you know (
attention): the times, Free time's cells ("free"), and the layers above them, "slot" (a slot of time for you), "dnd" (do-not-disturb from its switch or a focus session) and "reachable" (Reachable, the first of the three states: every row of the people you know rings then, whatever the time; do-not-disturb.md), each frame cut where a layer begins or ends, with whether it is work or admin time (hours). Always through is no floor: its people's numbers are rows of their own, "always-" and their own state ("always-neutral"), read in each frame, never over a blocked number (blocked beats Always through);floors.peoplestays empty, kept for an older table. Version 2 (19 October 2026): the frames name the people you know only; numbers nobody knows and hidden numbers are the screening's (screening, what can never be a caller's among it: 089, nothing of ARCEP's list of blocks), Expecting a call isexpecting. This Java reads version 2 and version 1, a row a frame does not name ringing ("no-row"), a column it does not know only said in the list of calls declined, a table of version 2 without its screening screening nobody, and a word of the screening it does not know (a later Sioul's) as letting the call ring, never declining it. Checked on the JVM against Rust'sdecidewith tables as Rust writes them (android/jvm-checks/TableCheck.java, both versions, the words not known;DecideCheck.java, every case ofcalls::tests::the_screening_decides_numbers_nobody_knows_apartand of the review's). - Emergency calls: whether Android shows Sioul your outgoing call to an emergency number cannot be tried without calling one (Android's code suggests it does: research, open question 5). If it does not, the day after is not known, and the other floors still hold: emergency services calling back from their numbers, a second call within 15 minutes.
- Expecting a call (until 19 October 2026 "Let every call through", which let every call ring, the people you know at night included): numbers nobody knows, hidden numbers and numbers you called lately ring while it lasts, even where your contacts wait; never the blocked, an automated caller, a number that cannot be a caller's or a failed verification; the people you know still as their rows say. On the background service's notification while this phone screens calls (Expecting a call, 1 h, Expecting a call; while on, Screen calls again); in the card What reaches you now, behind the status line's button, on every device while one of your phones screens calls (
ReachCard.qml: Expecting a call (1 h), or Screen calls again; on the phone, Call screening settings…; its on/off switch stays in Settings ▸ This phone). An older device's press of Let every call through is read as one of Expecting a call: the same press, in the same place. Each press is its device's own, in its table of do-not-disturb's switch file ([device.<id>.calls], carried by the sharing as do-not-disturb's switch is), the latest of all devices holding. Java keeps the notification's press at once (calls/through-here.json), and Rust carries it into the switch at the next step, with Java's time (calls::carry). - The log of calls screened: Java writes every incoming call it screened, declined or let ring, in
files/state/sioul/calls/held.jsonl, a month at most (Calls.held, once Android has its answer: the ringing never waits): when, its number's key and the number as the network gave it, hidden or not, who it was by your lists, the phone's contacts' name when Sioul's address books did not know it, the time of day (the table's frame then, even for a call decided before it), why (the screening's own: "screening", "automated", "impossible", "unverified", "expecting", "screening-off", "work-hours"), whether it rang (rang), the network's verification, and whether it counted as your contacts because you called it lately (called). That bit travels with the line: your other devices learn that you had called that number in the 14 days before it called (a person's sheet says so), nothing more of your outgoing calls. The Porch says a declined call for what it is ("an automated caller called at 10:12") and why on request, the screening's, never the time's. Never an outgoing call. Rust copies each line into this phone's own log,calls/log/<device>.jsonl(calls::before_exchange, before each exchange, the background step's and a dose's alarm's included, and when the Porch reads its list), leaving out the number as given, its presentation and a blocked caller's calls; the sharing carries it, sealed, a line at a time, to your other devices (the part "Calls", database.md, "The calls' logs"). Java never writes the shared log: the sharing may rewrite it, and Java cannot take Rust's lock (flock; Java's are POSIX record locks). The Porch of every device lists the declined calls (porch.md, "Calls declined"); a person's sheet shows their calls of the month. Checked on the JVM byandroid/jvm-checks/LogCheck.java(Calls.line,Calls.append: declined and rang, the frame's column, hidden, no table, never an outgoing call, the trim at a month; and the numbers called,Calls.rememberCalledandCalls.calledAt: 14 days at most, nothing with the setting off), and its lines read back by Rust (calls::tests::java_s_lines_read_here, withSIOUL_CALLS_SAMPLE;tools/final-pass.shruns both). - Voicemail by mail: Free mails each voicemail with its sound, when set to; Sioul links it to its call and plays it on the Porch (
sioul_core::voicemail). Nothing is transcribed (a later option: research 5.3). - Not filtered: texts (Sioul is not the SMS app): held as other apps' messages are, by who wrote, once Sioul has the notification access (below), a text from a number nobody knows from a root for machines gathered with the automatons; Android's own blocked numbers stop a number's calls and texts for every app (the Porch's list and Settings ▸ This phone open that page; only the phone or SMS app writes it).
- Not tried on a phone yet: built and checked on a computer (Rust's tests; the Java compiled against
android-36).
Notifications from other apps¶
Other apps' notifications are held until their time, as your mail is (attention.md: the Messages rows, the automatons' and At once's; research: research/android-chats.md, 7 and 8). Messages between people (SMS, chats, mail apps, a missed call) come when the person who wrote may reach you; notifications from automatons (shops, news, social networks, your browser's sites) come back at the gathered times, three a day unless you set others (Settings ▸ Reminders ▸ Gathered at, the same times as your sites' on a computer). Code: crates/sioul-core/src/appnotes.rs (what a notification is, when it comes, the words), crates/sioul-app/src/appnotes.rs (Android's side, the tab, the Porch's line), android/package/src/com/aurelienpierre/sioul/AppNotes.java (the listener).
- The listener: Android's
NotificationListenerService, bound by the system once you give Sioul "Notification access", in a process of its own (:listener), with Sioul's library and without Qt's window, as for the doses. Not the window's process: Qt's activity ends its whole process when it closes (System.exit(0)inQtActivityBase.onDestroy), which would unbind the listener at each close and let notifications pass unheld until Android binds it again. Not the background service's (:steps): that one runs only while its own setting is on, and does long work (the sync app's twenty seconds, mail) a notification must not wait for; and one failing must not silence the other. Its cost, one more process with Sioul's library, about 30 to 50 MB (estimated as for:steps), is to measure on the phone. - Holding is snoozing, never cancelling: Sioul asks Android to hold the notification until a time (
snoozeNotification); Android brings it back then, with its own exact alarm, even while Sioul sleeps, whole: its tap, its actions, its conversation; an update the app makes meanwhile replaces it, still held; one the app takes away (read on another device) is gone. Sioul never cancels one and never touches typing: holding it, the apps and the people in them see nothing (AOSP'sSnoozeHelper, read for this). It answers, or marks as read, only what you ask from a computer, through the notification's own Reply or Mark as read (Replies from your computers): the app then does what it does for a reply typed on the phone, read receipts included where its settings send them. - Two kinds, per notification, not per app:
- Between people: a conversation (
MessagingStyle, Android's conversations and their shortcuts), a message, a mail or a missed call by its category, people named with a number or an address, an SMS app's sender. Who wrote, as far as the app says it: a number or an address in the notification (tel:,mailto:), a contact of the phone's address book (its lookup address, read with your leave to read contacts: someone you know, even when Sioul's own address books lack them), an SMS app's sender number; else a name. Then who may reach you when: the one matrix (attention.md), its Messages rows (Mail's for a mail app, Calls' for a missed call), with your safe, neutral (in your address books), restricted and blocked people and those you do not know. - From an automaton: everything else, an SMS from a short number or a sender named in letters ("AMELI") that no address book knows included, and a sender that says it is a machine (Android's
Person.isBot: a bank's assistant). Gathered: at the next gathered time when nothing of Sioul's own would come (not while you sleep, pause, take Free time or have do-not-disturb on), and within the app's area when you gave it one.
- Between people: a conversation (
- Names: an app that names people as the phone's address book does (SMS apps, WhatsApp, the phone's own for a missed call) is taken at its word. A name anyone can take (Discord, Telegram, Signal for strangers, Proton Mail's senders) may only hold more: a name that is a restricted or blocked contact's holds as theirs; a safe or neutral contact's name counts as someone you do not know. "Always through" goes by conversation, never by name.
- Groups (a WhatsApp group, a Discord server's channel; a conversation where two people or more write, when the app does not say) are judged as the conversation, since anyone in it may write: as people you do not know, unless you choose for it. A conversation can be always through (at any time, sleep, pauses and do-not-disturb included: the school's group), gathered with the automatons, or held for good (never).
- Always through, during Sioul's modes: Sioul holds nothing of such a conversation, but Sioul's own mode would silence it, letting through no conversation, only starred people. Sioul's modes let priority conversations through instead (Android 11 and later, Pauses), and the tab helps mark one so: its button, beside a conversation set to Always through, opens the conversation's page in Android, where Priority is (
AppNotes.call, "open-conversation", by the conversation's key; "conversations" says, per key, whether Android marks it important already and whether its page can open). The listener keeps, for each conversation with a shortcut seen lately, where that page is: its app, its shortcut, its channel (the parent's when Android gave it one of its own), whether Android takes it as a conversation, marks it important, and has given it a channel of its own yet (sioul-conversations, Sioul's private storage, a week unseen at most, 300 at most), keyed as Rust keys the conversation (appnotes::talk_key, FNV-1a of its app and shortcut; the JVM checks Java's against Rust's,android/jvm-checks/ModeCheck.java). The page:ACTION_CHANNEL_NOTIFICATION_SETTINGSwith the app, the channel andEXTRA_CONVERSATION_IDonce the conversation has a channel of its own (it was changed once); before that, Android's list of conversations (android.settings.CONVERSATION_SETTINGS), where it shows among the recent ones, then the app's notifications. A long press on its next notification, Priority, does the same. Only the conversations Always through should be priority: Android rings every priority conversation through Sioul's modes before the listener hears of it. Android 10 has no conversations: there, a conversation Always through rings through a mode only from a starred contact. - Per app: notification by notification (the usual), all messages between people (an old SMS app without conversations), an automaton (all gathered), or at once (as Sioul's own notifications: not while you sleep, pause, take Free time or have do-not-disturb on); and what it is for (work, your admin, leisure), as an address or a site.
- By time, per app and per conversation: each app, and each conversation where the app says one, has a row of its own in the matrix (attention.md, §1.3, "Other apps' own rows"), its cells At once, At the gathered times, Held or As usual at each of the seven times and under the two layers: "Discord: through during Work, held otherwise". Shown in What reaches you ▸ Exceptions as a table (the apps seen in the last month; the conversations chosen for and the ten seen last), each app's line saying its row in a sentence. As usual until set: nothing changes for an app nobody chose for. A conversation's cell wins over its app's; someone Always through stays so, the blocked never come, codes and what is never held stay as they are. Decided as the rest is (
Attention::decide, the notification'sEvent::from_sources), at once now or where things change, gathered at a gathered time (appnotes::first_time); worked out again when the settings change, as any held notification is. Where Android cannot follow: a held app's first sound rings before Sioul sees it (made silent below, as for any app held); during a pause, Free time or do-not-disturb Sioul's own mode decides what rings, so what a cell lets through then shows without a sound, unless it is a priority conversation or a starred contact's message. The rows travel with the settings (an app's with its name), so a computer's Exceptions shows the apps' rows set on the phone and changes them. - Your browser's sites: Chromium's browsers name the site in each notification (its tag, its channel), Firefox's in its sub-text: each site is an automaton of its own, gathered, or at once when you say so.
- Mail apps (Proton Mail, Gmail, K-9…): the sender's name and the address it came to; that address's area when it is one of your accounts' (unsaid: work). Proton Mail names the sender, not their address (unless they gave no name): someone you do not know, unless the name is a restricted or blocked contact's.
- Never held: Sioul's own; ongoing notifications (music, a call in progress, navigation, a download) and those that cannot be cleared; calls ringing and alarms (a full-screen intent, their category); the reminders and events you set in other apps; group summaries (Android takes a summary with its last child); codes, and sign-ins or payments to approve: the notification's words read as mail's codes are (
codes::detect), sign-in and payment approvals ("Validez votre paiement", "Is this you?", 3-D Secure), a channel named for codes or sign-ins (Proton Mail's "Login"), and a copy Android redacted because its own assistant judged it sensitive (Android 15: Sioul then gets the app's name and Android's own words in place of the text, which is reason enough). - Sound: Android plays a notification's sound and shows its banner before it tells a listener (
NotificationManagerService: the sound, then the listeners): Sioul can hold a notification, not its first sound. Chosen: the apps Sioul holds are made silent by you in Android's settings, guided: the tab lists, per app and channel, what rang before Sioul held it, with a button to that channel's page. Chats and SMS may keep their sound, so that the people let through ring: one held then rings once, and waits out of sight. During a pause, Free time and do-not-disturb (its switch; your sleep and focus sessions too when What reaches you ▸ Do not disturb says so), Sioul's own mode silences every app but your starred contacts' messages and priority conversations, as the matrix says (Pauses): never all your contacts', since the mode is what keeps a held message silent. Not chosen: Android's global hint to silence every app (it would silence the apps Sioul does not see too), and Sioul ringing itself for what it lets through (left to decide). - In the shade, without a sound (◑; part 4 of the overrides, decision 13 of 18 October 2026): under do-not-disturb, the texts of your safe people, your contacts and groups are let through while the phone is silenced, so that they reach the shade without a sound, and held otherwise, so that nothing rings where the cell says silent. The listener knows it from Android: at a notification, its ranking (
intercepted: it does not match the interruption filter, Sioul's mode or yours); at a review, the filter now, since Android ranks no held notification and a held text cannot be asked whether it would ring (AppNotes.textsSilenced, said assilenced; an older listener says nothing, read as not silenced): "nothing" and "alarms only" silence every text; "priority" only when the policy in force lets neither messages nor conversations ring, which Android 15 and later say (getConsolidatedNotificationPolicy, Sioul's mode or yours). The limit (review of part 4, S-2): under "priority" on an older Android, which may let your starred contacts' texts ring, a text held waits until do-not-disturb ends, as before part 4; and an app whose channel you set to override do-not-disturb rings whatever the filter, at posting as at a review. Mail apps' notifications never go in the shade (S-1): their ◑ cells hold them on the phone, as before. Android tells a bound listener of every change of its filter: the listener then reviews what it holds, and what waited comes once the phone is silenced (onInterruptionFilterChanged). Starred contacts and priority conversations ring as Android's filter lets them. - Released as you asked (part 3 of what reaches you, do-not-disturb.md): Reachable pressed on any device, or "Let what your phone holds through" on a computer, stamps a release in the switch's file, which the listener watches. At its next review it applies each device's stamps once, apart (
Ledger::apply_releases): what it first held at or before the stamp comes now (held again on its return meanwhile included,Held::first), re-snoozed for one second, never cancelled; what it holds after the stamp waits as usual; a stamp ahead of the phone's clock reads as the phone's now, one more than a day ahead is passed over and said once in the log. Never during a pause: the stamp waits for its end (review of part 3, F5). The blocked and what is held for good never come. While Reachable lasts, everyone not blocked comes at once (Ask::reachable); apps' automatic notifications keep their gathered times. The log says "released as you asked". - What waited comes back at its time, alerting as its channel says (silent if you made it so). Sioul works its time out again when something moves it: a pause or Free time ended, do-not-disturb turned off, the hours, who may reach you, your lists of senders, Health's meals and nights, today's slots of time for you or the choices changed (the listener watches those files, and asks Android to bring what may come now within a second). Held with no moment found in the coming days (in a pause, which lasts until you come back), it is asked about again six hours on, and brought back as soon as the pause ends. Held again within minutes of its return three times in a row (the times worked out disagree with the time it is), it is let through rather than held again and again; long holds never count. A blocked sender's, or a conversation set to never, is held a week at a time. What is in the shade already when Sioul starts seeing notifications stays as it is: only what comes after is held.
- On the Porch, one line while the Porch is open: "Notifications held until 13:00: Discord, Le Monde and two other apps." and, for two hours after, "Back in your notifications at 13:00: …". Names and words, never a count past twelve.
- Calendars and address books in the phone's log (
adb logcat -s sioul | grep calendars:, 21 October 2026): at the window's start, how many contacts-and-calendars accounts this device has ("calendars: 1 contacts-and-calendars account(s) here, synced while the window is open"; with none, it says the calendars shown are this device's own and those your other devices share, none of a server's); as each account's sync starts, whether its homes are known here and its password kept here (yes or no only); after each sync, counts and a code, never a name nor an address: "calendars: 3 found (0 read only, 0 for tasks only), 3 synced, 0 new or renamed; address books: 1 found, 1 synced; first problem: none" (a calendar holding no event yet is offered for a new one like the others; only a read-only one is not, listed in Settings ▸ Agenda fixed, its row saying why), or "calendars: the account's sync stopped: sync-error-no-password; it waits for a change (a password given, Sync now) before trying again" (pim::watchers_line,pim::sync_line). The phone syncs its server's calendars itself, while the window is open only: the background step does not. An account switched off keeps its folders out of the pages through a mark of this device's own (vdir::set_account_off); the marks follow the configuration, which travels, at the window's start and after each sync of an account switched on (vdir::follow_switches): before 21 October 2026, an account switched on again on another device kept its mark on the phone, and its calendars, synced, never showed ("calendars: an account's calendars were marked off here, though it is on: shown again"). A sync that makes, renames or recolours a collection reads the pages again, with no item come yet (Report::registered). - In the phone's log (
adb logcat -s sioul), one calm line at each review of what is held (its reason: Sioul's files changed, the listener bound) and at each release (one held come back at its time): how many are held and how many come now, by the kind of their channel (calls, messages, mail, other apps); why, the matrix's column now and its layers ("leisure + dnd", do-not-disturb with what holds it: "dnd (focus session)", "dnd (switch)"), or "Always through" when that let one come; and the next release. "Notes: review (files): held 3 (messages 2, other apps 1); released 0; why: sleep; next release tomorrow 07:00." Never a name, a number, an app nor a message's words (appnotes::log_line;appnotes::tests::the_log_line_says_kinds_counts_and_times_only). When Rust cannot answer, the line says so: "what is held could not be worked out again; each notification keeps the time it had." Messages held longer than you expected show there, with why. - A line at each hold, from 19 October 2026: the kind of its channel, until when it waits (or "for good"), and what decided it at that moment (
appnotes::cause, thenhold_line): the columns of the matrix that hold it now, the time's or a layer by itself ("sleep", "work + admin", "slot", "dnd (focus session)", "free (nothing at all)"); its conversation's or its app's own row; what its app or the address a mail came to is for ("area (leisure)"); a gathering and what gathers it ("gathered" for an automaton, "gathered (its app)", "gathered (its conversation)", "gathered (a sender named only)": a text whose sender came as a name no card has, taken for a business's); "never" for a blocked sender or a conversation set to never, without saying which. "Notes: held: messages 1 until 10:50; why: dnd (focus session)." One Android brought back at its time and held again says "held again". The same rule: never a name, a number, an app, a conversation nor who wrote, not even their list (appnotes::tests::the_hold_line_says_what_decided_it_and_names_no_one; that something always says why:appnotes::tests::what_held_one_is_said). - Without the access, nothing is held: other apps' notifications come as they are sent, and only do-not-disturb holds them, yours or Sioul's.
- The access: Settings ▸ This phone says whether it is given, and the steps for Sioul installed from a file (an APK, "guarded" by Android 13 and later), in their order: Notification access ▸ Sioul, tapped once (Android may answer "Restricted setting": only after that does App info's ⋮ offer the next step), then Android ▸ Apps ▸ Sioul ▸ ⋮ ▸ Allow restricted settings, confirmed with the PIN; then Notification access ▸ Sioul ▸ on. Each with a button to its page; the first shows from Android 13 on, whether Android keeps restricted settings from Sioul or not, which no app can read (What Android withholds). On the same page, Android 12 and later let you narrow what Sioul sees, by app and by kind; Sioul asks for conversations, alerting and silent ones, never ongoing ones (
default_filter_types,disabled_filter_typesin the manifest). - What Sioul keeps, on this phone only, never shared with your other devices, never logged: your choices (
app-notes.tomlin its configuration; the rows by time are in[attention], shared with the settings, an app's with its name, a conversation's by its key alone, never its title); what it held, by a hash of Android's key, with when it comes back; the names of the apps, conversations and sites seen in the last weeks (a week for conversations, unless you chose for them), and of the channels that rang while held (app-notes-seen.tomlin its state); where each conversation's page is in Android, a week (above). Never what a notification says: its words are read in memory to decide, then dropped, except for the apps you send to your computers while that part of the sharing is on (Messages on your computers). - Android's record: Android notes a listener that takes a notification away within five seconds of its posting (
OP_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER, a defence against apps hiding codes). Sioul holds at once, so it is on that record; nothing in Android itself acts on it. - Time for you: the listener reads today's slots of time for you (capacity.md) from
state/slots.toml, which the window writes when it lays out the day and they changed (attention::Slots), at each notification and each review, and watches that file. A slot is the matrix's layer there as everywhere (attention.md): as usual, other apps' automatons, the apps set to come at once and mail apps' notifications wait it out (mail is shown there, not told; a phone cannot show what it holds), but your Always through people's, and texts and chats follow the time; a held notification's time is worked out with the slots' ends among the moments where things change (Ask::also). - Tried on a phone (8 October 2026, Android 12), with test notifications posted from adb (
adb shell cmd notification post -S messaging --conversation Test --message Test:Hello sioul-test Hello): one held at a meal as the usual matrix says (a stranger's message), until the meal's end; its app set to At once for meals in the table by time, it came back within four seconds; its conversation set to Held for meals, a second one was held, though its app said at once; both cells back to As usual. The table at the phone's width (412 points): the times' names upright, each app's name above its marks. Built and checked on a computer too (Rust's tests,cargo test -p sioul-core appnotesandcargo test -p sioul-app appnotes; the Java compiled in the APK,android-36).
Messages on your computers¶
One rule changed on 8 October 2026: a notification's words may now leave the phone, sealed through your sharing, for the apps you choose and while you turn this on; until then, and for every other app, they never do. The phone's messages, texts first, then reach your computers' Porch (porch.md), at the times their sender may reach you. Research: research/sms.md (phase a; phases b, reading the conversations and sending from the computer, and c, Sioul as the SMS app, are not built). Code: crates/sioul-core/src/phonemsgs.rs (what travels, the padding, the trims, the Porch's lines), crates/sioul-app/src/phonemsgs.rs (the listener's side, the Porch, the phone's tab), the listener's AppNotes.java.
- The switch: the sharing's part Messages from your phone (
phone-messages), off until you turn it on, on each device: on the phone (Settings ▸ This phone ▸ On your computers, or Settings ▸ Your folder and sharing) to send, on each computer that should show them to receive. A computer at work, or shared, never starts showing your texts by itself. Calls differ: their part is on by default. - By app (the same tab,
phone-messages.tomlin the phone's configuration, never shared): not on your computers, who and when, or who, when and the words. The phone's SMS app (Android's default one,Telephony.Sms.getDefaultSmsPackage) sends its words unless you say otherwise; every other app (Signal, WhatsApp, Discord, a mail app…) sends nothing until you choose it. - What travels, a line per message, in this phone's own log (database.md): when it was sent (the message's own time,
MessagingStyle.Message.getTimestamp), the app, the conversation's key and a group's title, who wrote as the app named them and their number (fromtel:or the phone's address book), the row the listener judged them by, the words cut at 1,000 characters, "a picture" for a picture, and when the phone let the notification through (its hold's end, so that a computer never shows a message the phone still holds; let through more than a minute earlier than first said, when a pause ends early, the hours change or a conversation was answered from a computer, the line is written again with that time and read once, at its earliest:phonemsgs::carry_into,phonemsgs::read_file). A hold made longer after its line was written is not carried: a computer shows that message at the first time. A service's text (a short number, "AMELI") travels with its words, as an automaton's, at the times the phone holds it. - Never: a code or an approval (
appnotes::is_code; a copy Android redacted counts as one): the line says "A code came from your bank at 09:12. It stays on your phone."; a picture, a voice note or an icon (said, never carried: itscontent://address stays on the phone); a blocked sender's messages; a conversation set to never; a notification its app marks secret (Notification.VISIBILITY_SECRET); ongoing ones, summaries, calls ringing and alarms, a missed call (the calls' part carries calls); Sioul's own; any button, reply field or tap (aPendingIntentis the phone's alone: a reply from a computer names its notification, and the phone finds that notification's Reply again, Replies from your computers). - When: the listener writes the line under Rust's lock right after deciding the notification (
phonemsgs::noteinsioul_appnotes_decide; Java never writes a shared file); a conversation's notification posted again brings its earlier messages again, which make no second line (each line's id is its time and a hash of its words). Java then asks the background service for a step 20 seconds on (StepService.SOON, at most one waiting, and only while the service runs; its step sends at once, without waiting for the sync app), so that the messages that follow go with it. With Sioul's window on the screen, the window's own exchanges send it. - Seen, on a computer, takes Sioul's line away on every device (each device's own seen log), with ten seconds to undo. It never touches the phone's notification and never marks anything read: in an RCS chat, marking read would tell the sender. Mark as read in Signal, in the line's menu, is the notification's own, asked apart, after its warning (Replies from your computers).
- Kept a week: the phone takes its lines out after 7 days, any device another phone's after 10.
- The price: the words sit sealed on your server and unsealed in Sioul's state on each computer that shows them; the key is on every device. Each line is padded to steps of 256 bytes, so that the server learns no message's length.
- Checked: Rust's tests (
cargo test --release -p sioul-core phonemsgs,-p sioul-sync messages); the Java compiled and the JVM checks (android/jvm-checks/run.sh). Not checked yet with the phone's own SMS app: /e/OS's Message is expected to give the sender's number intel:and the contact's name (research 1.1); a picture's address is never opened.
Replies from your computers¶
A reply written on a computer to a message your phone's notification brought goes through that notification's own Reply, as the phone's shade sends one typed under it: the app sends it as it sends a reply typed on the phone, with what it does then (it shows it in the conversation; Signal, Telegram and /e/OS's Message mark the conversation read). Built on 18 October 2026: the phone's side, the request decided once, the Porch's box, the switch per app and the twenty minutes; not tried on a phone yet (Not done yet). The design: 21 decisions, each with its recommendation (research of 17 October 2026). Code: crates/sioul-core/src/replies.rs (the rules), crates/sioul-app/src/replies.rs (the listener's side, the phone's step, the Porch's answers), crates/sioul-app/qml/PhoneMessagesSection.qml (the Porch's box), the listener's AppNotes.java and Replies.java.
- Which notification: each message's line for your computers names the notification it came in by the phone's own name for it, a hash of its app and Android's key (
replies::notice,AppNotes.talkKey); Android's key, whose tag may hold a number, never leaves the phone; the name is a hash, not a secret: a device holding the passphrase could find a tag by trying, and your devices alone read it. At sending, the listener finds that notification again, live, among what Android shows and then what Sioul holds, of the phone's own profile, never a work profile's (whose notifications offer your computers nothing); none: "gone". Rust then checks that it is still the conversation answered (replies::check): the same shape (a group or not, and a group's title), then its key as the line said it, else the same sender's number, else name; another conversation under that notification, or a line that names none of these, never passes: "other", never a guess. An app reusing one notification for several conversations is caught where the notification tells them apart (a shortcut, a conversation title, the group flag); a group's reply is refused ("other") when the group's key came from the one who wrote last, which names a person, not a group (two untitled groups where she wrote last look alike). What a notification never tells cannot be caught: a group without the group flag that shows one person's messages only looks like her conversation with you. Never a copy kept from before (KDE Connect replies through notifications already gone). - Which action (
Replies.choose): the Reply is the action Android says is one (SEMANTIC_ACTION_REPLY) with an input that takes free text, looked for among the notification's own actions, then the car extender's invisible ones, then the watch's; when no action says it is one, the single action whose input takes free text, only in a notification Android takes as a conversation or the phone's SMS app's (two such would be a guess: none). Mark as read: the action that says it is one (SEMANTIC_ACTION_MARK_AS_READ), in the same places. The first place that holds one decides; two in it ("Reply to the group" and "Reply privately", two Mark as read) would be a guess too: none, never the first in the list. Never one the system added (contextual), never one found by its title or its place: a Reply and a Like with swapped titles, or in another order, change nothing. - Never anything else (
Replies.refusal): no other action is ever sent (Delete, Archive, Block, which /e/OS's Message files under Mute, Call, which dials, a bot's buttons, a code's Copy); no notification is tapped, dismissed or cancelled. Refused, and said: an action whose PendingIntent starts an activity, which would open the app on the phone ("activity"); a Reply whose PendingIntent is immutable, which would drop the words without a word from Android ("closed"); one whose app asks for an unlocked phone (isAuthenticationRequired) while the phone is locked ("locked"). Otherwise a reply goes while the phone is locked, as Android Auto's and a watch's do. Nothing is offered below Android 12, whose actions do not say what these refusals need. - How it is sent, as SystemUI sends a reply typed in the shade: a fill-in intent for the foreground's receivers, the words under the first free-text input's key (
RemoteInput.addResultsToIntent), marked typed by a person (setResultsSource,SOURCE_FREE_FORM_INPUT), thensend; a Mark as read with no fill-in. No start of an activity is asked for. The notification's own allowance comes with the copy the listener holds, so that the app may use the network while the phone sleeps. Android saying the intent is spent or cancelled: "gone". - Who does what: the background step decides each request once and claims it, then hands what is due to the listener in a private file sealed with a key made from the sharing key (
state/sioul/replies/due.jsonl,ReplySeal, HKDF "sioul-replies 1"), which the listener watches. The listener alone holds the notifications: for each, "taken" is written and flushed before anything is sent (a crash after it leaves a doubt, never a second sending), the words come to Java in memory only then (and leave the due file, written again without them), and what happened is written beside (done.jsonl), never the words: handed, gone, other, no Reply now, closed, locked, activity, late (not taken within two minutes of the step's claim: the listener was not bound), failed; with whether their newer messages crossed the reply (by the messages' own times: a notification that gives none says nothing of it), and whether it was answered among the notifications Sioul holds. "Shown" when, within two minutes, the app's notification shows your words as yours: a message of yours timed after the reply was taken; where it gives no time, your words in it or in the Reply's own history, only when they were not there before it went (an earlier "Oui" of yours never makes a later "Oui" shown); compared by a hash, in memory. - Asked, once (
replies::Ask,replies::verdict; decisions 2, 3, 16 and 19): a computer writes each reply in its own file,phone-messages/ask/<computer>.jsonl, a random key of 32 hex digits, the computer that wrote it (sealed with it: a line found in another device's file than its writer's is passed over, so that neither the server nor a stray file moves a request from one computer's name to another's), the phone, the verb (reply,readfor the notification's Mark as read,cancel), the notification's name, the app, the line answered (its id in that phone's log), the words (2,000 characters at most; none forread), when it was written and how far its clock may run ahead, and the key it sends again (Send again). Each line is padded to 256 bytes and sealed with the replies' key before the sharing seals it again; the file is also copied into the sharing folder and sent first, on the lane of the texts to send (database.md), so that a reply reaches the phone within its quarter of an hour whatever the records' size. Nothing else can be asked: there is no field for an action, an app's key or an address. - Decided once, by the texts' rules, at the phone's step (before each exchange, and right after one that brought requests), under the lock both its processes take: another phone's, nothing; a key the phone's ledger holds already, nothing, whatever the sharing reads again; taken back by its computer before the phone claimed it ("taken back"); asked by a device the sharing no longer counts, by its sealed writer (left, forgotten, replaced, silent:
share::counting, which reads sealed facts only), refused; refused too, and said, another verb, no words or too many, a line this phone's log does not hold or of another notification, a code's line, an app whose Answer from your computers is off on the phone, a short number or a sender named in letters in the SMS app (it may cost money); written before the ledger's mark, refused (the mark is made at every phone step, before the requests are read, and when the listener relays a line for your computers: a reply answers such a line, so the first reply is written after it; only a ledger lost and made again refuses what came before); dated more than two minutes ahead of the phone's clock ("clocks"); older than 15 minutes on the phone's clock ("expired"); then the two guards: the same words to the same conversation within ten minutes of a reply that went or may still go (to the same notification, or to the same conversation by its key, so that the conversation's newer notification, when its app posted it anew, finds the first too; claimed, taken, handed, shown, in doubt; never one the phone said did not go: gone, other, no Reply, closed, locked, activity, late, failed), unless the request sends that first one again on purpose, and more than 60 replies claimed in an hour from all computers, refused; a Mark as read, sixty an hour too ("too-many-read"). A refusal for the same words says what is known: that they went (the phone said so), or that they are on their way (claimed, taken, in doubt: Send again then goes only if the first did not; it goes through the conversation's notification now, and the guard finds the first through any notification of the conversation). Otherwise it is claimed in the phone's private ledger (state/sioul/replies/ledger.jsonland its mark: the notification's name and a hash of the words, never the words). At most 100 requests are decided a step, each computer's in turn (the rest at the next: a runaway computer's thousands never keep another's reply from its turn); every decision of the step is written in the ledger together and flushed once, the claims among them, and only then handed to the listener and said; the ends of the claims the listener settled ("late", "doubt", then "done" in the ledger) are written together too, one flush each a step, however many; each request is found once by its key in a set, the ledger's keys too, so that a computer's thousands decided already cost a read, not a search each; a decision whose word could not be written is said at the next step (replies::ends_of). Claimed and never taken within two minutes, it is "late"; taken with no word after, "doubt": its app may have it. Sioul never sends one again by itself; Send again makes a new key. Take back, while it waits: acancelof its key from the same computer, which the phone files as taken back unless it claimed it already; the computer says at once that it is being taken back ("Taking it back: your phone will not send it, unless it has taken it already."), and, when the phone had taken it already, that Take back came too late. - What became of it (
replies::Outcome), in the phone's ownphone-messages/outcome/<phone>.jsonl, each state once, never the words: taken, handed, shown, or why it was not sent; a computer says it in words (replies::state_of), never a plain "Sent", which Sioul cannot know of another app: "Waiting for your phone (it last shared at 14:02).", "Your phone is handing it to Signal.", "Handed to Signal at 14:03." (and "It crossed a newer message of theirs, above.", or, answered among those held, that a newer message of theirs waits on the phone), "Mark as read handed to Signal at 14:03." (handed: whether the app then marks it is its own), "Signal shows it in the conversation.", "Your phone may have handed it to Signal: look in Signal before you send it again.", each reason it was not sent with what to do: Send again after it expired, clocks apart or a doubt; "Reply on your phone" when the notification is gone, holds another conversation, or its Reply is closed to Sioul. - A week: the requests and outcomes are taken out by their writer after 7 days, by any device 10 days on; the phone's private files of what is due and done after a day, its ledger's keys after 30.
- What a notification offers your computers (
replies::Live): for each notification it relays, the phone says in its ownphone-messages/live/<device>.jsonlwhether it offers a Reply it can fill and a Mark as read, and when it went (Android's removal; Sioul's own holding is no going); a line per change, never one per message, no words, no names; taken out with the log after a week (database.md). - The switch per app, on the phone: Settings ▸ This phone ▸ On your computers, under each app that sends, Answer from your computers, on by default (
phone-messages.toml'sno_answers, the apps switched off; never shared). Off, the phone's live lines say that app's notifications offer nothing, and a request for it is refused, said: "Not sent: answers from your computers are off for Signal on your phone." - On a computer's Porch (porch.md;
replies::answer_of): Reply first on a line whose phone says its notification's Reply works (the conversation's newest notification, when its app posted it anew under another name, answered by that notification's own newest line, of the same shape: a group or not, and the same title, so that a group's words never go to the one-to-one of the one who wrote last; none of its lines here, "Reply on your phone."); a box under it, "Reply to Alice on Signal…", "Reply in École on WhatsApp…" for a group, Send (Ctrl+Enter); your reply under their messages, "You · J'arrive.", with its state in words, Take back while it waits (said at once, and said too late when the phone had taken it already), Send again where it fits; "Reply on your phone." once the notification is gone or its Reply closed to Sioul. The line stays until Seen, so a second reply goes while the notification lives; their next message makes the next line. The first reply in Signal, Telegram or /e/OS's Message says once that replying marks the conversation read there, as from the phone's notification (decision 13). Mark as read in Signal, in the line's menu, only where the notification offers one and after its warning, "Signal may tell them that you read it."; once handed, the line is seen on every device. A person's sheet offers no reply. - A text, the texts' way (decision 12): for a text from a whole number between two, where the phone it came to sends texts for this computer (its texts read here), Reply sends one of Sioul's own texts from that phone (texts.md), never from another phone's SIM, with its 1,600 characters and its states ("Sent at 14:03.", "Delivered at 14:03."), whether the notification is there or not; a group text, or a text whose phone does not send texts for this computer, goes through the SMS app's own Reply; never to a short number or a sender named in letters.
- Their answer to your answer (decision 18): a conversation answered from a computer comes through at once on the phone for 20 minutes after the reply was handed, as a second call within a quarter of an hour rings; never during a pause; the blocked and a conversation set to never stay held (
Decision::answered,Why::Answered). Kept in the phone's private replies folder (state/sioul/replies/answered.json): the conversation's key, a hash, and until when; a text a computer asked for counts too, once Android has it, by a hash of the number, for the SMS app's notifications (replies::answered_number). What Sioul holds of that conversation comes at once too: the listener works what is held out again when that file changes, so that a reply to a notification Sioul held (decision 17) brings it back. On the computer that answered, the Porch shows that conversation's lines at once too, as the phone lets them through, but in a pause (replies::Answered). - Agents never reply (decision 20): no tool of Sioul's MCP server sends a reply or a Mark as read;
phone_messagesstays read-only (mcp.md). - Who may ask: only a device the sharing counts now, by the writer sealed in each request. That rests on the passphrase, as everything shared does: any device that holds it can write a request in any name, and a computer you forgot counts again as soon as it shares anything newer than the forgetting (a record, or its entry, written at least once an hour while it runs: sealed facts only, never its plain notes,
share::present): Forget this device stops a silent device, not a running one. Only a new passphrase cuts a device off. The server can hold a request back or serve an old one again (decided once by its key; past its quarter of an hour, or before the mark, refused), never write one, change one or move one to another computer's name. - In the phone's log: states only, never a word, a name or an app ("Replies: one handed, crossing newer messages.").
- Checked:
cargo test --release -p sioul-core replies(the name, taken once and never late, the conversation checked, shown, the live lines; a request padded and sealed, its writer sealed in it and a line moved to another computer's file passed over, decided once step by step with its guards, the same words again after one that never went, and through the conversation's newer notification while the first may still go, the mark made once before the first request, settled late or in doubt, each state an outcome once and said in words, Take back said at once and too late; a reply never into a conversation of another shape; shown only by a message timed after it; a Porch's line's answer, through the notification, the conversation's newer one by its own line (of the same shape: a group's never to her one-to-one), or as a text from the phone it came to, with this computer's replies under it, and what it offers sent by the phone's verdict; the twenty minutes, on the phone and on the computer that answered;-p sioul-core phonemsgs, a message let through earlier written again),-p sioul-app replies(the phone's step on stand-in folders: the first reply after the install from an empty private folder, a reply gone then the same words again, a decision's lost word said again),-p sioul-sync(the seal; the live lines reaching the desk sealed; a reply decided once and its outcome back; the replies on the texts' lane; a forgotten computer counted again only by a sealed fact), the JVM checks (android/jvm-checks/RepliesCheck.java: the Reply and the Mark as read of stand-ins shaped as Signal's, WhatsApp's, Telegram's with a bot's buttons, Element X's, /e/OS's Message's with Delete, Block and Call, K-9's; an immutable Reply, one that opens the app, one that asks for an unlocked phone; swapped titles and another order; two that say Reply, or two Mark as read, in one place: none; the car extender's and the watch's; the notification by its name, gone, or another profile's), the Porch's box (crates/sioul-app/tests/qml/tst_phonemessages.qml: Reply first and its box, a text as a text, your reply with Take back, Send again through the conversation's notification now, Reply on your phone, Mark as read after its warning, the switch per app). Not yet on a phone.
Texts¶
Your phone's texts read on your computers, and texts written there sent by your phone, without Sioul being the SMS app: texts.md says how, with every rule that keeps a text from being sent twice. On the phone, Settings ▸ This phone ▸ Texts (TextsSetup.qml) asks Android for READ_SMS, RECEIVE_SMS, SEND_SMS and READ_PHONE_STATE, each with its sentence, through an activity never seen (TextsAsk.java); never for the SMS role.
- The reader (
Texts.java): the whole history, a batch at a time, the newest first by date (Rust keeps the rows its log holds, each by its id and its date: texts.md): the provider's inbox and sent box, 500 rows at most, every column; multimedia messages with every column of their rows, addresses and parts (a part's_data, its file's place on the phone, left out), each part's size from Android; each conversation's people from Android's list of its recipients. A part's content copied into Sioul's private folder when Rust asks (texts-part), which Rust puts in the sharing as a sealed blob and deletes. At each step, every row there is, each id with its date (texts-ids:smswithsms_at,mmswithmms_at; a table that cannot be listed whole is left out, and nothing is read or marked deleted for it), then the rows its log lacks, by their ids (texts-readwithsms_ids,mms_ids); once a day the same listing marks the texts deleted on the phone and says the sizes (measure). Handed to Rust as JSON (crates/sioul-app/src/texts.rs): the import after the background service's exchange, or in a thread of the window's, never inside an exchange (texts.md). A new text (SMS_RECEIVED,Texts.Heard, in:steps, sent by the system alone) brings a step 20 seconds on, while the background service runs. The service watches Android's provider of texts (Texts.watch, aContentObserveron the texts and multimedia messages, onceREAD_SMSis given): a text received, sent, deleted or read marks them changed, and the import asks Android only then, and once a day all the same (texts-changes, which also says whether the phone is on its charger); a message received, its inbox's newest id grown (a multimedia message once downloaded, which no broadcast tells Sioul), brings a step 20 seconds on too (Texts.arrived), a text sent waits for the next step; a heavy import, the first or one a full batch left behind, goes on only while the phone charges (texts.md). - The sender (
TextSend.java): a text Rust claimed in the private ledger first, handed to Android from the SIM asked or the conversation's last; each part's sent and delivered results toTextSend.Result(in:steps), written to Java's own file, flushed, copied by Rust into the phone's outcomes. - Multimedia messages from a computer (
TextSend.sendMms; texts.md): no new permission (SEND_SMS), never the SMS role. Rust builds the message on the phone, measures it against the SIM's carrier (Texts.limits,SmsManager.getCarrierConfigValues, which asks no permission: never values of Sioul's over the carrier's), writes it in the private texts folder, claims it, then Java hands Android its address in Sioul's file provider (content://<package>.qtprovider/files_path/…, the provider Qt's FileProvider already is: Android reads it through the grantsendMultimediaMessagemakes) with an explicit, mutable intent for its word (Android adds the carrier's answer, the HTTP status and the row it filed). Android files it in the conversation with Sioul as its creator and the message's key astr_id; a claim with no word is looked for by that key 40 minutes on (Texts.findMms, Sioul's rows alone). Multimedia messages always leave over the mobile network: mobile data off, Android says so, and Sioul says it in words. Java's results (texts/results.jsonl, a line or two a message) are read for the outcomes' month alone and trimmed to it once a day, set aside by a rename and never written over, since Java appends to the file by its name from either process (trim_results; the re-check, I-f).texts-find-mmsgives each of Sioul's rows with the key, its box and the carrier's status Android read in its answer (resp_st), orerrorwhen the provider's query fails. Settings ▸ This phone ▸ Texts shows what each SIM's carrier takes (texts-limits, read when the tab is shown), read only. - The step's order: before each exchange, under one lock for both processes, Android's results copied, the requests decided and sent, a claim left without Android's word looked for among the texts Sioul sent; after the exchange, the import's next batch and its media (when the texts changed; a history on the charger only, its newest texts on the battery too), and the day's look for deletions. When something was written, the step shares it at once (
steps.rs). - Settings ▸ This phone ▸ Texts says how many texts and megabytes of media the phone holds, then how much has gone (and, off the charger during a heavy import, that the rest follows while the phone charges), and chooses the largest media file brought to your computers (10 MB by default).
- What Sioul keeps on the phone, never shared (the state folder's
texts/): the reader's state and measures (reader.json, version 4: the rows its log holds, the rows that came and the history still to read, each as[id, date]), the ledger and its mark (ledger.jsonl,ledger-mark.json, keys kept 30 days), Java's results (results.jsonl), how Android answered the texts' last question (asked.json: how many given, how fast), a part's content for a moment (incoming/). - Checked: Rust's tests, the JVM checks (
android/jvm-checks/TextsCheck.java, on a stand-in provider: every column of a text, a multimedia message's addresses and parts without_data, a part copied, the ids, a part's result, a multimedia message's result, its file's address in the provider, a SIM's carrier values;MmsCheck.java: the multimedia messages Rust builds read by Android's own parser). On the owner's phone, the reader's first version read its texts (counts only looked at); the archive's version is checked there by counts and sizes only.
What Android withholds¶
From Android 13, an app installed from a file, not through a store, gets no notification access until you allow its restricted settings; from Android 15, Android keeps the texts' permissions from it the same way (and the SMS role, which Sioul never asks). Android then refuses without showing its question: the first time, it shows its own "Restricted setting" instead; you then allow them in App info ▸ ⋮ (at the top) ▸ Allow restricted settings, confirmed with the phone's PIN, and the question can be asked again. The owner's new phone (16 October 2026, Android 16 on /e/OS, Sioul installed by adb, then from the GitHub APK) refused every texts permission that way, without a word, and READ_CONTACTS had not been given; notification access stayed greyed. Its old phone, on Android 12, had none of this. That first dialog marks the app (the app op ACCESS_RESTRICTED_SETTINGS goes from "errored" to "ignored"); only then does App info's ⋮ offer Allow restricted settings. On Android 15 and later, the requests after are refused without any dialog (the review of 16 October 2026, from AOSP's sources: Settings' AppInfoDashboardFragment, the permission controller's GrantPermissionsActivity, EnhancedConfirmationService).
- Never read, said either way (
Texts.java): no app can read whether Android keeps restricted settings from it: the app op is restrict-read on every release that has it (MANAGE_APPOPS and the like, which no app a person installs holds), and Android 15's enhanced confirmation asks a permission of its own. So, on Android 15 and later for the texts (13 and later for notification access, which Android 13 and 14 alone keep so), once the texts' question was asked here (asked: Java's own filetexts/asked.jsonin the private texts folder, written byTextsAsk) and the three permissions are not all given, the lines say the steps that hold either way, in their order: Allow in Android… once (Android may answer "Restricted setting"); then Sioul's app info ▸ ⋮ ▸ Allow restricted settings, confirmed with the PIN, and Allow in Android… again; if ⋮ offers nothing, the permissions under Permissions there. Before Android 15 for the texts (13 for notification access), a question answered at once with nothing given (under 0.7 s: "Don't allow" chosen twice before;Texts.atOnce,at_once) leads to Sioul's app info and its Permissions. The three texts' permissions are asked alone, timed; the SIMs' names (READ_PHONE_STATE), which Android never keeps behind restricted settings, are asked after them, so that a quick answer for them never hides a question shown. Notification access gets the same order on Android 13 and later: Allow in Android… and Sioul tapped once in Notification access, then ⋮ ▸ Allow restricted settings, then Sioul turned on there. - Said where you look, in plain words, with what Android wants and a button for each: Settings ▸ This phone ▸ Texts (under Allow in Android…: what to do in App info, and Sioul's app info) and ▸ Other apps (the two steps, the first only when it applies); the join's result, under what still needs a sign-in (
JoinPanel.qml); and the Porch, once (AccessCard.qml: Not now puts it away for those things for good, a view flag each,porch-access-left-<what>; a thing Android allows leaves it; Android's state read only while the Porch is in view). Never counted, never red. What is said (crates/sioul-app/src/access.rs): the texts' permissions while the part Texts is on, and, in the join's result, that the part Texts is off, with its switch, on a phone that could read texts (the part is off on every device until turned on); notification access, only while other apps' notifications are held or the phone's messages go to your computers (appnotes::Choices::hold, the part Messages from your phone); the contacts where calls are screened or the phone's messages go to your computers. - The buttons: Allow in Android… asks Android again (the texts' question, the notification access page, the contacts' question); Sioul's app info opens Android's App info for Sioul (
ACTION_APPLICATION_DETAILS_SETTINGS,texts-info), where the ⋮ menu and the permissions are. While Android's page is open, the line is read again every two seconds, half a minute at most. - Checked:
access::tests::what_android_withholds_is_said_with_what_it_wants(each state's lines, in English and French), the JVM checks (TextsCheck.java:Texts.atOnce), the QML tests (tst_access.qml: the Porch's card, its buttons and Not now, read only in view;tst_join.qml: the join's lines, the part Texts' switch). Not yet on a phone: the steps' order on Android 13 to 16 (Android's "Restricted setting", then ⋮), and the time of a refusal without a question.
Not done yet¶
- Notifications: the doses', the time running's, the alarm at waking's, the events' reminders, new mail's, and the end of the day's (above); Health's other notices (the meals', the naps', the pause to move), codes, the sites' and the other reminders (dates asked, waits, payments, papers) have none on Android yet: the full card on the home screen says them, from their time (The card on the home screen). Nothing tries them on a phone: until 16 October 2026 the pause to move, on by default, tried a desktop notice every 45 minutes, and its refusal showed in the status line ("notifications are not made on Android yet"). Every caller whose error would reach the status line now stops on Android first (
backend::tests::no_notice_tried_on_a_phone_says_its_error_in_the_status_line, a check of the sources). Other apps' notifications are held, not yet tried on a phone (above). - Replies from your computers: built, not tried on a phone yet (Replies from your computers). With you there: the first reply after the install (never "before-mark"); one reply in Signal (handed, then shown; the notification still there with your reply last; a second reply; the conversation read), in WhatsApp, in /e/OS's Message (and the same through the texts' way, from that phone, with sent and delivered), in Telegram or Element where you use them; one with the phone locked and still for half an hour; a reply while the conversation's notification is held (its newer message then comes at once); Mark as read in Signal; Take back before the phone takes a reply; the time from Send to "Handed" and to "Shown".
- Multimedia messages from a computer: built on both sides (the phone: the message, its measure, its sending, its word, the look by its key from 40 minutes to Android's last word at 100, the carriers' values, shown read only on Settings ▸ This phone ▸ Texts; the computer: what you attach made fit, Settings ▸ Texts, the page: texts.md); not tried on a phone yet (Texts). With you there, counts, sizes, times and states only, never a message's content: your carriers' values said by the phone (expected for a French carrier: 614,400 bytes, 2592 × 1944), with nothing sent; whether mobile data is on, and /e/OS's words when a message needs it; a first message of some 100 kB with words to a number you choose (Android's code, the carrier's status, the time from the request to "handed" to "sent", the row in the sent box with its key as
tr_id, the message in /e/OS's Message, the picture whole on the other phone with your number as its sender); one just under the limit; airplane mode during a send (Android's wait, then its code 3); a restart while Android waits ("Your phone is still trying to send it" past 40 minutes, "doubt" 100 minutes on); Sioul stopped after "handed" (the word still comes); how long the media take to reach the phone through eDrive and through Sioul's own fetch, against the quarter of an hour; the message read back by the import with its parts, the same blobs, shown once. Two more, counts only (the review of 18 October 2026): after the first send, the message found by its key in the provider (texts-find-mms: one row, box 2, Sioul its creator), the never-twice path after a crash, which a send that goes well never takes; and whether the carrier's answer came with Android's result (confin Java's result line, read by its length alone), so that the look in the sent box stays rare. - Touch: the pages are laid out for a phone and their menus come at a long press, but their targets are still sized for a mouse.
- Sioul's modes, as the matrix says: built and checked on a computer (Rust's tests of what each mode asks and of the line, the Java compiled against
android-36, its choices of policy and channel checked on a JVM,android/jvm-checks/ModeCheck.java), not tried on a phone yet. The button to a conversation's page in Android is built too: in Settings ▸ This phone, beside a conversation set to Always through that Android does not hold as important, it opens that conversation's page, to mark it a priority there (AppNotesSetup.qml; the verb "open-conversation" through Rust,appnotes::change, to Java). - In the background: the service's cost measured again on a phone after the changes of 9 and 10 October 2026 (In the background); with it off, Android suspends an app it does not show, and changes and mail come when Sioul opens.
- Files from Android's pickers come as
content://addresses, which the core does not read yet (the writing window's Attach among them: a file shared to Sioul is copied, Sharing); downloads stay in the app's own folder, but for a mail attachment's Save…, which asks Android where (Attachments). - Other programs Sioul calls on a computer (Tesseract for paper letters, ClamAV for attachments) do not exist on a phone: attachments open there unchecked, and say so (Attachments).
- Permissions asked at the time (the microphone for memos, the position for maps), as Android wants them.
- Signing: with no key in the repository's secrets, each build is signed with a key of its own, so a phone takes the next one for another app (uninstall first). A store needs a key kept for good, and Google Play an Android App Bundle; F-Droid builds everything from source, Qt included.