tauri_plugin_background_service/
notifier.rs1use crate::models::PluginConfig;
8use tauri::{AppHandle, Runtime};
9use tauri_plugin_notification::NotificationExt;
10
11#[derive(Clone)]
15pub struct Notifier<R: Runtime> {
16 pub(crate) app: AppHandle<R>,
17}
18
19impl<R: Runtime> Notifier<R> {
20 pub fn show(&self, title: &str, body: &str) {
25 if let Err(e) = self
26 .app
27 .notification()
28 .builder()
29 .title(title)
30 .body(body)
31 .show()
32 {
33 log::warn!("background-service: notification failed: {e}");
34 }
35 }
36
37 pub fn show_with_id(&self, id: &str, title: &str, body: &str) {
43 if let Err(e) = self
44 .app
45 .notification()
46 .builder()
47 .id(stable_notification_id(id))
48 .title(title)
49 .body(body)
50 .show()
51 {
52 log::warn!("background-service: notification {id} failed: {e}");
53 }
54 }
55}
56
57pub(crate) fn stable_notification_id(id: &str) -> i32 {
61 let mut hash: u32 = 0x811c_9dc5;
62 for byte in id.as_bytes() {
63 hash ^= u32::from(*byte);
64 hash = hash.wrapping_mul(0x0100_0193);
65 }
66 hash as i32
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
74pub struct NotifierPolicy {
75 pub on_timeout: bool,
77 pub on_recovery: bool,
79}
80
81impl NotifierPolicy {
82 pub fn derive(config: &PluginConfig, is_android: bool) -> Self {
94 let native_owns_timeout = is_android && config.android_on_timeout == "notifyUser";
95 Self {
96 on_timeout: config.notify_on_timeout && !native_owns_timeout,
97 on_recovery: config.notify_on_recovery && !is_android,
98 }
99 }
100}
101
102pub trait NotifySink: Send + Sync {
109 fn notify(&self, id: &str, title: &str, body: &str);
111}
112
113impl<R: Runtime> NotifySink for Notifier<R> {
114 fn notify(&self, id: &str, title: &str, body: &str) {
115 self.show_with_id(id, title, body);
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122 use crate::models::PluginConfig;
123
124 #[allow(dead_code)]
127 fn notifier_clone_compiles<R: Runtime + Clone>(app: AppHandle<R>) {
128 let n = Notifier { app };
129 let _cloned = n.clone();
130 }
131
132 #[allow(dead_code)]
135 fn notifier_show_with_id_compiles<R: Runtime>(n: &Notifier<R>) {
136 n.show_with_id("bg-timeout", "title", "body");
137 }
138
139 #[allow(dead_code)]
141 fn notifier_is_notify_sink<R: Runtime>(n: Notifier<R>) -> std::sync::Arc<dyn NotifySink> {
142 std::sync::Arc::new(n)
143 }
144
145 #[test]
146 fn stable_notification_id_is_deterministic() {
147 assert_eq!(
148 stable_notification_id("bg-timeout"),
149 stable_notification_id("bg-timeout")
150 );
151 assert_ne!(
152 stable_notification_id("bg-timeout"),
153 stable_notification_id("bg-recovery")
154 );
155 }
156
157 fn config(
160 notify_on_timeout: bool,
161 notify_on_recovery: bool,
162 android_on_timeout: &str,
163 ) -> PluginConfig {
164 PluginConfig {
165 notify_on_timeout,
166 notify_on_recovery,
167 android_on_timeout: android_on_timeout.into(),
168 ..Default::default()
169 }
170 }
171
172 #[test]
173 fn derive_desktop_honors_configured_keys() {
174 let policy = NotifierPolicy::derive(&config(true, true, "notifyUser"), false);
175 assert_eq!(
176 policy,
177 NotifierPolicy {
178 on_timeout: true,
179 on_recovery: true
180 }
181 );
182 }
183
184 #[test]
185 fn derive_desktop_defaults_off() {
186 let policy = NotifierPolicy::derive(&config(false, false, "notifyUser"), false);
187 assert_eq!(
188 policy,
189 NotifierPolicy {
190 on_timeout: false,
191 on_recovery: false
192 }
193 );
194 }
195
196 #[test]
197 fn derive_android_notify_user_suppresses_timeout() {
198 let policy = NotifierPolicy::derive(&config(true, true, "notifyUser"), true);
201 assert_eq!(
202 policy,
203 NotifierPolicy {
204 on_timeout: false,
205 on_recovery: false
206 }
207 );
208 }
209
210 #[test]
211 fn derive_android_stop_keeps_timeout() {
212 let policy = NotifierPolicy::derive(&config(true, true, "stop"), true);
215 assert_eq!(
216 policy,
217 NotifierPolicy {
218 on_timeout: true,
219 on_recovery: false
220 }
221 );
222 }
223
224 #[test]
225 fn derive_android_schedule_recovery_keeps_timeout() {
226 let policy = NotifierPolicy::derive(&config(true, true, "scheduleRecovery"), true);
227 assert_eq!(
228 policy,
229 NotifierPolicy {
230 on_timeout: true,
231 on_recovery: false
232 }
233 );
234 }
235
236 #[test]
237 fn derive_android_always_suppresses_recovery() {
238 let policy = NotifierPolicy::derive(&config(false, true, "stop"), true);
241 assert_eq!(
242 policy,
243 NotifierPolicy {
244 on_timeout: false,
245 on_recovery: false
246 }
247 );
248 }
249
250 #[test]
251 fn derive_default_policy_is_all_off() {
252 assert_eq!(
253 NotifierPolicy::default(),
254 NotifierPolicy {
255 on_timeout: false,
256 on_recovery: false
257 }
258 );
259 }
260}