use crate::models::PluginConfig;
use tauri::{AppHandle, Runtime};
use tauri_plugin_notification::NotificationExt;
#[derive(Clone)]
pub struct Notifier<R: Runtime> {
pub(crate) app: AppHandle<R>,
}
impl<R: Runtime> Notifier<R> {
pub fn show(&self, title: &str, body: &str) {
if let Err(e) = self
.app
.notification()
.builder()
.title(title)
.body(body)
.show()
{
log::warn!("background-service: notification failed: {e}");
}
}
pub fn show_with_id(&self, id: &str, title: &str, body: &str) {
if let Err(e) = self
.app
.notification()
.builder()
.id(stable_notification_id(id))
.title(title)
.body(body)
.show()
{
log::warn!("background-service: notification {id} failed: {e}");
}
}
}
pub(crate) fn stable_notification_id(id: &str) -> i32 {
let mut hash: u32 = 0x811c_9dc5;
for byte in id.as_bytes() {
hash ^= u32::from(*byte);
hash = hash.wrapping_mul(0x0100_0193);
}
((hash & 0x7FFF_FFFF) as i32) | 1
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct NotifierPolicy {
pub on_timeout: bool,
pub on_recovery: bool,
}
impl NotifierPolicy {
pub fn derive(config: &PluginConfig, is_android: bool) -> Self {
let native_owns_timeout = is_android && config.android_on_timeout == "notifyUser";
Self {
on_timeout: config.notify_on_timeout && !native_owns_timeout,
on_recovery: config.notify_on_recovery && !is_android,
}
}
}
pub trait NotifySink: Send + Sync {
fn notify(&self, id: &str, title: &str, body: &str);
}
impl<R: Runtime> NotifySink for Notifier<R> {
fn notify(&self, id: &str, title: &str, body: &str) {
self.show_with_id(id, title, body);
}
}
#[cfg(test)]
#[allow(clippy::field_reassign_with_default)]
mod tests {
use super::*;
use crate::models::PluginConfig;
#[allow(dead_code)]
fn notifier_clone_compiles<R: Runtime + Clone>(app: AppHandle<R>) {
let n = Notifier { app };
let _cloned = n.clone();
}
#[allow(dead_code)]
fn notifier_show_with_id_compiles<R: Runtime>(n: &Notifier<R>) {
n.show_with_id("bg-timeout", "title", "body");
}
#[test]
fn stable_notification_id_is_deterministic() {
assert_eq!(
stable_notification_id("bg-timeout"),
stable_notification_id("bg-timeout")
);
assert_ne!(
stable_notification_id("bg-timeout"),
stable_notification_id("bg-recovery")
);
}
#[test]
fn stable_notification_id_is_always_positive_and_nonzero() {
let mut seen = std::collections::HashSet::new();
for i in 0..1024u32 {
let key = format!("bg-key-{i}");
let id = stable_notification_id(&key);
assert!(id > 0, "id for {key} must be positive, got {id}");
assert_eq!(id, stable_notification_id(&key));
seen.insert(id);
}
assert!(
seen.len() > 900,
"expected wide separation, got {} distinct ids",
seen.len()
);
}
#[test]
fn stable_notification_id_representative_values_are_pinned() {
assert_eq!(stable_notification_id("bg-timeout"), 0x4E26_122D);
assert_eq!(stable_notification_id("bg-recovery"), 0x3D4C_C8B3);
let a = stable_notification_id("bg-timeout");
let b = stable_notification_id("bg-recovery");
assert!(a > 0 && b > 0);
assert_ne!(a, b);
}
fn config(
notify_on_timeout: bool,
notify_on_recovery: bool,
android_on_timeout: &str,
) -> PluginConfig {
PluginConfig {
notify_on_timeout,
notify_on_recovery,
android_on_timeout: android_on_timeout.into(),
..Default::default()
}
}
#[test]
fn derive_desktop_honors_configured_keys() {
let policy = NotifierPolicy::derive(&config(true, true, "notifyUser"), false);
assert_eq!(
policy,
NotifierPolicy {
on_timeout: true,
on_recovery: true
}
);
}
#[test]
fn derive_desktop_defaults_off() {
let policy = NotifierPolicy::derive(&config(false, false, "notifyUser"), false);
assert_eq!(
policy,
NotifierPolicy {
on_timeout: false,
on_recovery: false
}
);
}
#[test]
fn derive_android_notify_user_suppresses_timeout() {
let policy = NotifierPolicy::derive(&config(true, true, "notifyUser"), true);
assert_eq!(
policy,
NotifierPolicy {
on_timeout: false,
on_recovery: false
}
);
}
#[test]
fn derive_android_stop_keeps_timeout() {
let policy = NotifierPolicy::derive(&config(true, true, "stop"), true);
assert_eq!(
policy,
NotifierPolicy {
on_timeout: true,
on_recovery: false
}
);
}
#[test]
fn derive_android_schedule_recovery_keeps_timeout() {
let policy = NotifierPolicy::derive(&config(true, true, "scheduleRecovery"), true);
assert_eq!(
policy,
NotifierPolicy {
on_timeout: true,
on_recovery: false
}
);
}
#[test]
fn derive_android_always_suppresses_recovery() {
let policy = NotifierPolicy::derive(&config(false, true, "stop"), true);
assert_eq!(
policy,
NotifierPolicy {
on_timeout: false,
on_recovery: false
}
);
}
#[test]
fn derive_default_policy_is_all_off() {
assert_eq!(
NotifierPolicy::default(),
NotifierPolicy {
on_timeout: false,
on_recovery: false
}
);
}
}