Skip to main content

sioul_app/
alarms.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2// Copyright © 2026 Aurélien Pierre
3
4//! Doses on a phone, reminded while Sioul is not on the screen (docs/health.md,
5//! "On a phone"): Android freezes an app in the background, so each coming
6//! dose is given to Android's alarm clock (android/main.cpp); at its time a
7//! receiver asks here what to say (`health::alarm_decide`), after reading what
8//! your other devices marked: taken, nothing; not known, the doubt said first.
9//! Elsewhere, nothing here: the window's minute reminds.
10
11use std::ffi::{CStr, CString, c_char};
12
13/// The doses last given to Android, so that they are given again only when they change.
14static GIVEN: std::sync::Mutex<String> = std::sync::Mutex::new(String::new());
15
16#[cfg(target_os = "android")]
17// SAFETY: declared as android/main.cpp defines them: extern "C", the same types.
18unsafe extern "C" {
19    /// Android's alarm clock given the coming doses, replacing those given before (android/main.cpp).
20    fn sioul_android_set_alarms(json: *const c_char);
21    /// A tapped reminder's dose, once; false when there is none (android/main.cpp).
22    fn sioul_android_take_opened(key: *mut c_char, size: i32) -> bool;
23    /// Whether Android lets Sioul set exact alarms (android/main.cpp).
24    fn sioul_android_exact_alarms() -> bool;
25    /// Whether Android shows Sioul's notifications (and its "Doses" channel is on).
26    fn sioul_android_notifications_allowed() -> bool;
27    /// The reminder shown for a dose taken away (android/main.cpp).
28    fn sioul_android_remove_reminder(key: *const c_char);
29}
30
31/// Doses marked whose reminder was taken away already, in this process.
32static REMOVED: std::sync::Mutex<std::collections::BTreeSet<String>> = std::sync::Mutex::new(std::collections::BTreeSet::new());
33
34/// The reminder shown for a dose taken away: once it is marked, here or on
35/// another device, a reminder left on the phone would say it is still to take.
36pub(crate) fn remove_reminder(key: &str) {
37    let Ok(text) = CString::new(key) else { return };
38    #[cfg(target_os = "android")]
39    // SAFETY: android/main.cpp's; `text` is a C string alive until the call returns, and it copies it.
40    unsafe {
41        sioul_android_remove_reminder(text.as_ptr())
42    };
43    let _ = text;
44    if let Ok(mut removed) = REMOVED.lock() {
45        removed.insert(key.to_string());
46    }
47}
48
49/// Whether reminders can show at all: on a phone, only when Android lets
50/// Sioul's notifications through; elsewhere, always.
51pub(crate) fn notifications_allowed() -> bool {
52    #[cfg(target_os = "android")]
53    // SAFETY: android/main.cpp's, without arguments; it may be called from any thread.
54    return unsafe { sioul_android_notifications_allowed() };
55    #[cfg(not(target_os = "android"))]
56    true
57}
58
59/// Whether reminders come at their minute: on a phone, only when Android lets
60/// Sioul set exact alarms; elsewhere, always.
61pub(crate) fn exact() -> bool {
62    #[cfg(target_os = "android")]
63    // SAFETY: android/main.cpp's, without arguments; it may be called from any thread.
64    return unsafe { sioul_android_exact_alarms() };
65    #[cfg(not(target_os = "android"))]
66    true
67}
68
69/// The coming doses given to Android's alarm clock, when they changed: at
70/// each minute of the window, when it is put away, and after a dose is marked
71/// or Health's settings saved; the alarm at waking with them (`wake`), which
72/// keeps its own list, codes and receiver.
73pub(crate) fn schedule() {
74    if !cfg!(target_os = "android") {
75        return;
76    }
77    crate::wake::schedule();
78    // The reminders of doses marked since (here, or come from another device) taken away.
79    let gone: Vec<String> = crate::health::marked_lately().into_iter().filter(|key| REMOVED.lock().is_ok_and(|removed| !removed.contains(key))).collect();
80    for key in gone {
81        remove_reminder(&key);
82    }
83    let coming = crate::health::alarms_coming();
84    let Ok(mut given) = GIVEN.lock() else { return };
85    if *given == coming {
86        return;
87    }
88    let Ok(text) = CString::new(coming.clone()) else { return };
89    #[cfg(target_os = "android")]
90    // SAFETY: android/main.cpp's; `text` is a C string alive until the call returns, and it copies it.
91    unsafe {
92        sioul_android_set_alarms(text.as_ptr())
93    };
94    let _ = text;
95    *given = coming;
96}
97
98/// The dose of a reminder tapped while Sioul was away, once.
99pub(crate) fn opened() -> Option<String> {
100    #[cfg(target_os = "android")]
101    {
102        let mut buffer = vec![0u8; 512];
103        // SAFETY: android/main.cpp's; it writes at most `buffer.len()` bytes into `buffer`, its zero included.
104        let found = unsafe { sioul_android_take_opened(buffer.as_mut_ptr().cast(), buffer.len() as i32) };
105        if found {
106            let end = buffer.iter().position(|b| *b == 0).unwrap_or(buffer.len());
107            return String::from_utf8(buffer[..end].to_vec()).ok().filter(|k| !k.is_empty());
108        }
109    }
110    None
111}
112
113/// A text handed to Android's side, freed by `sioul_string_free`.
114pub(crate) fn handed(text: String) -> *mut c_char {
115    CString::new(text).unwrap_or_default().into_raw()
116}
117
118/// A key from Android's side; "" when unreadable.
119///
120/// # Safety
121/// `key` is null, or a zero-terminated text valid for the call.
122pub(crate) unsafe fn key_of(key: *const c_char) -> String {
123    if key.is_null() {
124        return String::new();
125    }
126    // SAFETY: not null (above), and a C string valid for the call, as this function's contract says.
127    unsafe { CStr::from_ptr(key) }.to_string_lossy().to_string()
128}
129
130/// At a dose's time: what to say (`health::alarm_decide`). Blocks up to half
131/// a minute (the sync app given time to bring the others' marks): never on
132/// Android's main thread.
133///
134/// # Safety
135/// `key` is null, or a zero-terminated text valid for the call.
136// SAFETY: no other symbol of the program has this name.
137#[unsafe(no_mangle)]
138pub unsafe extern "C" fn sioul_alarm_decide(key: *const c_char) -> *mut c_char {
139    // SAFETY: `key` is null or a C string valid for the call, as this function's contract says.
140    let key = unsafe { key_of(key) };
141    let answer = std::panic::catch_unwind(|| crate::health::alarm_decide(&key)).unwrap_or_else(|_| {
142        // Something broke: never silence, never a claim. A dose is due, to be checked in Sioul.
143        let body = std::panic::catch_unwind(|| crate::backend::tr().text("dose-alarm-fallback", None)).unwrap_or_else(|_| "A dose is due: open Sioul to check it.".into());
144        serde_json::json!({ "show": true, "title": "Sioul", "body": body, "again_at": 0 }).to_string()
145    });
146    // The home screen's card says the doses as they are now (homecard.rs).
147    let _ = std::panic::catch_unwind(crate::homecard::doses_changed);
148    handed(answer)
149}
150
151/// "Taken" on a phone's reminder (`health::alarm_taken`).
152///
153/// # Safety
154/// `key` is null, or a zero-terminated text valid for the call.
155// SAFETY: no other symbol of the program has this name.
156#[unsafe(no_mangle)]
157pub unsafe extern "C" fn sioul_alarm_taken(key: *const c_char) -> *mut c_char {
158    // SAFETY: `key` is null or a C string valid for the call, as this function's contract says.
159    let key = unsafe { key_of(key) };
160    let answer = std::panic::catch_unwind(|| crate::health::alarm_taken(&key)).unwrap_or_else(|_| r#"{"done":false,"open":true,"line":""}"#.to_string());
161    // The home screen's card says the doses as they are now (homecard.rs).
162    let _ = std::panic::catch_unwind(crate::homecard::doses_changed);
163    handed(answer)
164}
165
166/// A text handed by `sioul_alarm_decide`, `sioul_alarm_taken`,
167/// `sioul_time_action` (timenote.rs) or `sioul_wake_next` (wake.rs), given back.
168///
169/// # Safety
170/// `text` is null, or came from one of them and is given back once.
171// SAFETY: no other symbol of the program has this name.
172#[unsafe(no_mangle)]
173pub unsafe extern "C" fn sioul_string_free(text: *mut c_char) {
174    if !text.is_null() {
175        // SAFETY: `text` was made by `handed` (CString::into_raw) and is given back once, as the contract says.
176        drop(unsafe { CString::from_raw(text) });
177    }
178}