Skip to content

Commit 74364e9

Browse files
Copilotgfauredev
andauthored
feat: Android notifications, lock-screen signal, keyguard detection (partial)
Agent-Logs-Url: https://github.com/gfauredev/LogOut/sessions/21f3bfed-afee-4983-99b1-e3faf77febd1 Co-authored-by: gfauredev <19304085+gfauredev@users.noreply.github.com>
1 parent 81df0e1 commit 74364e9

5 files changed

Lines changed: 442 additions & 4 deletions

File tree

src/components/session_timers.rs

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use crate::models::{format_time, format_time_i64, get_current_timestamp, Force};
22
use dioxus::prelude::*;
3-
#[cfg(target_arch = "wasm32")]
43
use dioxus_i18n::t;
54
/// Timer tick interval in milliseconds
65
#[cfg(target_arch = "wasm32")]
@@ -234,6 +233,25 @@ pub(super) fn RestTimerDisplay(
234233
schedule_notification_at(title, body, "logout-rest", target_unix_ms);
235234
}
236235
}
236+
// On native (Android), schedule a precise one-shot notification via tokio.
237+
#[cfg(not(target_arch = "wasm32"))]
238+
if let Some(start) = start_time {
239+
if rest_duration > 0 {
240+
let title = t!("notif-rest-title").to_string();
241+
let body = t!("notif-rest-body").to_string();
242+
let fire_at_secs = start + rest_duration;
243+
tokio::spawn(async move {
244+
let now = crate::models::get_current_timestamp();
245+
if fire_at_secs > now {
246+
let delay = std::time::Duration::from_secs(fire_at_secs - now);
247+
tokio::time::sleep(delay).await;
248+
}
249+
crate::services::android_notifications::send_notification(
250+
&title, &body, "logout-rest",
251+
);
252+
});
253+
}
254+
}
237255
}
238256
let Some(start) = start_time else {
239257
return rsx! {
@@ -255,6 +273,12 @@ pub(super) fn RestTimerDisplay(
255273
&t!("notif-rest-body"),
256274
"logout-rest",
257275
);
276+
#[cfg(not(target_arch = "wasm32"))]
277+
crate::services::android_notifications::send_notification(
278+
&t!("notif-rest-title"),
279+
&t!("notif-rest-body"),
280+
"logout-rest",
281+
);
258282
}
259283
}
260284
let remaining = rd.cast_signed() - elapsed.cast_signed();
@@ -295,6 +319,27 @@ pub(super) fn ExerciseElapsedTimer(
295319
}
296320
}
297321
}
322+
// On native (Android), schedule a precise one-shot notification via tokio.
323+
#[cfg(not(target_arch = "wasm32"))]
324+
if !*duration_bell_rung.read() {
325+
if let (Some(start), Some(dur)) = (exercise_start, last_duration) {
326+
if dur > 0 {
327+
let title = t!("notif-duration-title").to_string();
328+
let body = t!("notif-duration-body").to_string();
329+
let fire_at_secs = start + dur;
330+
tokio::spawn(async move {
331+
let now = crate::models::get_current_timestamp();
332+
if fire_at_secs > now {
333+
let delay = std::time::Duration::from_secs(fire_at_secs - now);
334+
tokio::time::sleep(delay).await;
335+
}
336+
crate::services::android_notifications::send_notification(
337+
&title, &body, "logout-duration",
338+
);
339+
});
340+
}
341+
}
342+
}
298343
});
299344
let mut now_tick = use_signal(get_current_timestamp);
300345
use_coroutine(move |_: UnboundedReceiver<()>| async move {
@@ -323,6 +368,12 @@ pub(super) fn ExerciseElapsedTimer(
323368
&t!("notif-duration-body"),
324369
"logout-duration",
325370
);
371+
#[cfg(not(target_arch = "wasm32"))]
372+
crate::services::android_notifications::send_notification(
373+
&t!("notif-duration-title"),
374+
&t!("notif-duration-body"),
375+
"logout-duration",
376+
);
326377
}
327378
}
328379
}
@@ -362,6 +413,27 @@ pub(super) fn InlineExerciseTimer(
362413
}
363414
}
364415
}
416+
// On native (Android), schedule a precise one-shot notification via tokio.
417+
#[cfg(not(target_arch = "wasm32"))]
418+
if !*duration_bell_rung.read() {
419+
if let (Some(start), Some(dur)) = (exercise_start, last_duration) {
420+
if dur > 0 {
421+
let title = t!("notif-duration-title").to_string();
422+
let body = t!("notif-duration-body").to_string();
423+
let fire_at_secs = start + dur;
424+
tokio::spawn(async move {
425+
let now = crate::models::get_current_timestamp();
426+
if fire_at_secs > now {
427+
let delay = std::time::Duration::from_secs(fire_at_secs - now);
428+
tokio::time::sleep(delay).await;
429+
}
430+
crate::services::android_notifications::send_notification(
431+
&title, &body, "logout-duration",
432+
);
433+
});
434+
}
435+
}
436+
}
365437
});
366438
let mut now_tick = use_signal(get_current_timestamp);
367439
use_coroutine(move |_: UnboundedReceiver<()>| async move {
@@ -390,6 +462,12 @@ pub(super) fn InlineExerciseTimer(
390462
&t!("notif-duration-body"),
391463
"logout-duration",
392464
);
465+
#[cfg(not(target_arch = "wasm32"))]
466+
crate::services::android_notifications::send_notification(
467+
&t!("notif-duration-title"),
468+
&t!("notif-duration-body"),
469+
"logout-duration",
470+
);
393471
}
394472
}
395473
}

src/main.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@ pub struct DbEmptyToastSignal(pub Signal<bool>);
6666
/// `None` when idle; `Some((downloaded, total))` while downloading images.
6767
#[derive(Clone, Copy)]
6868
pub struct ImageDownloadProgressSignal(pub Signal<Option<(usize, usize)>>);
69+
/// Global context signal that is `true` while the Android keyguard (lock screen)
70+
/// is active **and** the app is being shown over it (i.e. there is or was an
71+
/// active session). While this is `true`, all writes except those targeting the
72+
/// currently active session are refused.
73+
#[derive(Clone, Copy)]
74+
pub struct ScreenLockedSignal(pub Signal<bool>);
6975
#[derive(Clone, Routable, Debug, PartialEq)]
7076
#[rustfmt::skip]
7177
enum Route {
@@ -173,6 +179,7 @@ fn App() -> Element {
173179
use_context_provider(|| PendingDeepLinkSignal(Signal::new(None)));
174180
use_context_provider(|| ShowRestInputSignal(Signal::new(false)));
175181
use_context_provider(|| RestDurationSignal(Signal::new(DEFAULT_REST_SECONDS)));
182+
use_context_provider(|| ScreenLockedSignal(Signal::new(false)));
176183

177184
// Services that consume contexts (must run after context providers above).
178185
services::storage::provide_app_state();
@@ -190,6 +197,25 @@ fn App() -> Element {
190197
});
191198
}
192199

200+
// On Android: poll the keyguard (lock screen) state every second and update
201+
// `ScreenLockedSignal` so that write guards throughout the app can react.
202+
// The poll only runs while the app is shown over the lock screen (i.e. a
203+
// screen-wake-lock is held); otherwise the check is a no-op.
204+
#[cfg(target_os = "android")]
205+
{
206+
let mut screen_locked = consume_context::<ScreenLockedSignal>().0;
207+
use_coroutine(move |_: futures_channel::mpsc::UnboundedReceiver<()>| async move {
208+
loop {
209+
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
210+
let locked = services::wake_lock::is_shown_over_lock_screen()
211+
&& services::wake_lock::is_keyguard_locked();
212+
if *screen_locked.peek() != locked {
213+
screen_locked.set(locked);
214+
}
215+
}
216+
});
217+
}
218+
193219
#[cfg(not(target_arch = "wasm32"))]
194220
services::storage::native_queue::use_native_results();
195221
#[cfg(target_arch = "wasm32")]

0 commit comments

Comments
 (0)