1use crate::backend::{load_config, tr};
22use jiff::Zoned;
23use jiff::tz::TimeZone;
24use sioul_core::reminders::{self, Holds, Kind, Reminder};
25use std::ffi::{CString, c_char};
26use std::sync::Mutex;
27
28#[cfg(target_os = "android")]
29unsafe extern "C" {
31 fn sioul_android_set_event_alarms(json: *const c_char);
33 fn sioul_android_event_note(json: *const c_char);
35 fn sioul_android_mail_note(json: *const c_char);
37 fn sioul_android_take_reminder_opened(buffer: *mut c_char, size: i32) -> bool;
39}
40
41const AT_MOST: usize = 100;
43const LOOK_AGAIN: i64 = 24 * 3600;
45const LOOK_STEP: i64 = 6 * 3600;
47const AGAIN: i64 = 15 * 60;
49
50static GIVEN: Mutex<String> = Mutex::new(String::new());
52
53fn words() -> serde_json::Value {
56 serde_json::json!({
57 "channel": tr().text("events-channel", None),
58 "mail": tr().text("mail-channel", None),
59 "mail-through": tr().text("mail-through-channel", None),
60 "alarms": tr().text("alarms-channel", None),
61 "alarms-pause": tr().text("dnd-events-channel", None),
62 "open": tr().text("reminder-open", None),
63 })
64}
65
66pub(crate) fn coming(now: &Zoned) -> String {
73 let (all, holds) = reminders::gather_events(&load_config(), tr(), now);
74 listed(&all, &crate::hours::with_layers(holds), now)
75}
76
77fn listed(all: &[Reminder], holds: &Holds, now: &Zoned) -> String {
79 let stamp = now.timestamp().as_second();
80 let dir = reminders::told_dir();
81 let mut due: Vec<(i64, &Reminder)> = all
82 .iter()
83 .filter(|r| matches!(r.kind, Kind::Event | Kind::Before | Kind::Alarm) && r.until > stamp && !reminders::told(&dir, &r.key))
84 .map(|r| (when(r, holds, stamp), r))
85 .filter(|(at, r)| *at < r.until)
86 .collect();
87 due.sort_by_key(|(at, r)| (*at, r.key.clone()));
88 let list: Vec<serde_json::Value> = due.iter().take(AT_MOST).map(|(at, r)| serde_json::json!({ "key": r.key, "kind": r.kind, "at": at * 1000, "title": r.title, "body": r.body })).collect();
89 let look = (stamp / LOOK_STEP + LOOK_AGAIN / LOOK_STEP) * LOOK_STEP;
90 serde_json::json!({ "reminders": list, "look": look * 1000, "words": words() }).to_string()
91}
92
93fn when(reminder: &Reminder, holds: &Holds, stamp: i64) -> i64 {
96 if reminder.at > stamp {
97 reminder.at
98 } else if reminder.ready_in(stamp, holds) {
99 stamp + 1
100 } else {
101 again_at(holds, stamp)
102 }
103}
104
105fn hand(json: String) {
107 let Ok(mut given) = GIVEN.lock() else { return };
108 if *given == json {
109 return;
110 }
111 let Ok(text) = CString::new(json.clone()) else { return };
112 #[cfg(target_os = "android")]
113 unsafe {
115 sioul_android_set_event_alarms(text.as_ptr())
116 };
117 let _ = text;
118 *given = json;
119}
120
121pub(crate) fn schedule() {
124 if !cfg!(target_os = "android") {
125 return;
126 }
127 hand(coming(&Zoned::now()));
128}
129
130pub(crate) fn schedule_from(all: &[Reminder], holds: &Holds, now: &Zoned) {
133 if !cfg!(target_os = "android") {
134 return;
135 }
136 hand(listed(all, holds, now));
137}
138
139pub(crate) fn show(reminder: &Reminder) {
142 let json = serde_json::json!({
143 "key": reminder.key,
144 "kind": reminder.kind,
145 "title": reminder.title,
146 "body": reminder.body,
147 "event": reminder.target,
148 "until": reminder.until * 1000,
149 "words": words(),
150 });
151 let Ok(text) = CString::new(json.to_string()) else { return };
152 #[cfg(target_os = "android")]
153 unsafe {
155 sioul_android_event_note(text.as_ptr())
156 };
157 let _ = text;
158}
159
160#[cfg_attr(not(target_os = "android"), allow(dead_code))]
165pub(crate) fn mail_note(title: &str, body: &str, through: bool) {
166 let json = serde_json::json!({ "title": title, "body": body, "through": through, "words": words() });
167 let Ok(text) = CString::new(json.to_string()) else { return };
168 #[cfg(target_os = "android")]
169 unsafe {
171 sioul_android_mail_note(text.as_ptr())
172 };
173 let _ = text;
174}
175
176fn again_at(holds: &Holds, stamp: i64) -> i64 {
181 match holds.until {
182 Some(until) if !holds.now.slot && !holds.now.dnd => until.max(stamp + 60),
183 _ => stamp + AGAIN,
184 }
185}
186
187fn decide(key: &str) -> String {
192 let now = Zoned::now();
193 let stamp = now.timestamp().as_second();
194 let (all, holds) = reminders::gather_events(&load_config(), tr(), &now);
195 let holds = crate::hours::with_layers(holds);
196 let dir = reminders::told_dir();
197 let answer = match all.iter().find(|r| r.key == key) {
198 Some(r) if r.ready_in(stamp, &holds) => {
199 let shown = reminders::claim(&dir, &r.key);
201 if shown {
202 show(r);
203 }
204 serde_json::json!({ "shown": shown })
205 }
206 Some(r) if stamp < r.until && !reminders::told(&dir, &r.key) => {
208 let again = when(r, &holds, stamp).max(stamp + 1);
209 if again < r.until { serde_json::json!({ "again_at": again * 1000 }) } else { serde_json::json!({}) }
210 }
211 _ => serde_json::json!({}),
212 };
213 schedule();
214 answer.to_string()
215}
216
217#[unsafe(no_mangle)]
225pub unsafe extern "C" fn sioul_event_decide(key: *const c_char) -> *mut c_char {
226 let key = unsafe { crate::alarms::key_of(key) };
228 let answer = std::panic::catch_unwind(|| decide(&key)).unwrap_or_else(|_| "null".to_string());
229 crate::alarms::handed(answer)
230}
231
232#[unsafe(no_mangle)]
240pub unsafe extern "C" fn sioul_event_coming(zone: *const c_char) -> *mut c_char {
241 let zone = unsafe { crate::alarms::key_of(zone) };
243 let answer = std::panic::catch_unwind(|| {
244 let zone = TimeZone::get(&zone).unwrap_or_else(|_| TimeZone::system());
245 let json = coming(&Zoned::now().with_time_zone(zone));
246 if let Ok(mut given) = GIVEN.lock() {
247 *given = json.clone();
248 }
249 json
250 })
251 .unwrap_or_default();
252 let _ = std::panic::catch_unwind(|| crate::calls::refresh(false));
254 crate::alarms::handed(answer)
255}
256
257pub(crate) fn opened() -> Option<(String, String)> {
260 #[cfg(target_os = "android")]
261 {
262 let mut buffer = vec![0u8; 4096];
263 let found = unsafe { sioul_android_take_reminder_opened(buffer.as_mut_ptr().cast(), buffer.len() as i32) };
265 if found {
266 let end = buffer.iter().position(|b| *b == 0).unwrap_or(buffer.len());
267 let said: serde_json::Value = serde_json::from_slice(&buffer[..end]).ok()?;
268 let kind = said["kind"].as_str().filter(|k| *k == "event" || *k == "porch")?.to_string();
269 return Some((kind, said["key"].as_str().unwrap_or_default().to_string()));
270 }
271 }
272 None
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278
279 #[test]
280 fn held_reminders_are_asked_about_again_when_the_hold_ends() {
281 use sioul_core::reminders::Wait;
282 let stamp = 1_800_000_000;
283 let usual = Holds::usual;
284 assert_eq!(again_at(&usual(Wait::Sleep { from: stamp - 3600, until: stamp + 7200 }), stamp), stamp + 7200, "at waking");
285 assert_eq!(again_at(&usual(Wait::Free { until: stamp + 600 }), stamp), stamp + 600, "at the end of free time");
286 assert_eq!(again_at(&usual(Wait::Free { until: stamp }), stamp), stamp + 60, "never at once again");
287 assert_eq!(again_at(&usual(Wait::Paused), stamp), stamp + AGAIN);
288 let reminder = |at: i64, until: i64| Reminder { key: "before:train:1".into(), kind: Kind::Before, at, until, title: String::new(), body: String::new(), target: String::new(), work: false, starts: Some(until) };
290 let night = usual(Wait::Sleep { from: stamp - 8 * 3600, until: stamp + 900 });
291 assert_eq!(when(&reminder(stamp + 600, stamp + 3600), &usual(Wait::Nothing), stamp), stamp + 600);
292 assert_eq!(when(&reminder(stamp - 60, stamp + 3600), &usual(Wait::Nothing), stamp), stamp + 1);
293 assert_eq!(when(&reminder(stamp - 60, stamp + 3600), &night, stamp), stamp + 900, "kept in the list given, at waking");
294 let mut focus = usual(Wait::Nothing);
296 focus.now.dnd = true;
297 focus.attention.set(sioul_core::attention::Row::Own(sioul_core::attention::Kind::Before), sioul_core::attention::Column::Dnd, sioul_core::attention::Level::Later).unwrap();
298 assert_eq!(when(&reminder(stamp - 60, stamp + 3600), &focus, stamp), stamp + AGAIN);
299 }
300}