1use sync_async::sync_async;
2use crate::*;
3use super::*;
4
5
6#[sync_async]
7pub struct ProgressNotificationGuard<R: tauri::Runtime> {
8 #[cfg(target_os = "android")]
9 inner: Inner<R>,
10
11 #[cfg(not(target_os = "android"))]
12 inner: std::marker::PhantomData<fn() -> R>,
13}
14
15#[cfg(target_os = "android")]
16#[sync_async(
17 use(if_sync) impls::SyncImpls as Impls;
18 use(if_async) impls::AsyncImpls as Impls;
19)]
20impl<R: tauri::Runtime> ProgressNotificationGuard<R> {
21
22 #[maybe_async]
23 pub(crate) fn start_new_notification(
24 icon: ProgressNotificationIcon,
25 title: Option<String>,
26 text: Option<String>,
27 sub_text: Option<String>,
28 progress: Option<u64>,
29 progress_max: Option<u64>,
30 handle: tauri::plugin::PluginHandle<R>,
31 ) -> Result<Self> {
32
33 let impls = Impls { handle: &handle };
34
35 let (n_progress, n_progress_max) = normalize_progress_and_max(progress, progress_max);
36 let id = impls
37 .start_progress_notification(
38 icon,
39 title.as_deref(),
40 text.as_deref(),
41 sub_text.as_deref(),
42 n_progress,
43 n_progress_max
44 )
45 .await?;
46
47 Ok(Self {
48 inner: Inner {
49 current_state: std::sync::Mutex::new(CurrentState {
50 title: title.clone(),
51 text,
52 sub_text,
53 progress,
54 progress_max
55 }),
56 drop_behavior: std::sync::Mutex::new(DropBehavior {
57 title: None,
58 text: None,
59 sub_text: None,
60 share_src: None,
61 error: true,
62 }),
63 update_throttler: Throttler::with_delay(std::time::Duration::from_millis(1100)),
64 id,
65 icon,
66 handle,
67 is_finished: false,
68 }
69 })
70 }
71
72 #[always_sync]
73 fn impls(&self) -> Impls<'_, R> {
74 Impls { handle: &self.inner.handle }
75 }
76}
77
78#[sync_async(
79 use(if_async) api_async::{AndroidFs, Utils};
80 use(if_sync) api_sync::{AndroidFs, Utils};
81)]
82impl<R: tauri::Runtime> ProgressNotificationGuard<R> {
83
84 #[always_sync]
85 pub fn into_sync(self) -> SyncProgressNotificationGuard<R> {
86 SyncProgressNotificationGuard { inner: self.inner }
87 }
88
89 #[always_sync]
90 pub fn into_async(self) -> AsyncProgressNotificationGuard<R> {
91 AsyncProgressNotificationGuard { inner: self.inner }
92 }
93
94 #[always_sync]
95 pub fn title(&self) -> Option<String> {
96 #[cfg(not(target_os = "android"))] {
97 None
98 }
99 #[cfg(target_os = "android")] {
100 self.inner.lock_current_state().title.clone()
101 }
102 }
103
104 #[always_sync]
105 pub fn text(&self) -> Option<String> {
106 #[cfg(not(target_os = "android"))] {
107 None
108 }
109 #[cfg(target_os = "android")] {
110 self.inner.lock_current_state().text.clone()
111 }
112 }
113
114 #[always_sync]
115 pub fn sub_text(&self) -> Option<String> {
116 #[cfg(not(target_os = "android"))] {
117 None
118 }
119 #[cfg(target_os = "android")] {
120 self.inner.lock_current_state().sub_text.clone()
121 }
122 }
123
124 #[always_sync]
125 pub fn progress(&self) -> Option<u64> {
126 #[cfg(not(target_os = "android"))] {
127 None
128 }
129 #[cfg(target_os = "android")] {
130 self.inner.lock_current_state().progress
131 }
132 }
133
134 #[always_sync]
135 pub fn progress_max(&self) -> Option<u64> {
136 #[cfg(not(target_os = "android"))] {
137 None
138 }
139 #[cfg(target_os = "android")] {
140 self.inner.lock_current_state().progress_max
141 }
142 }
143
144 #[maybe_async]
145 pub fn update_progress_by(&self, addend: u64) -> Result<()> {
146 #[cfg(not(target_os = "android"))] {
147 Ok(())
148 }
149 #[cfg(target_os = "android")] {
150 {
151 let mut state = self.inner.lock_current_state();
152 state.progress = Some(state.progress.unwrap_or(0).saturating_add(addend));
153 }
154
155 self.update_notification().await?;
156 Ok(())
157 }
158 }
159
160 #[maybe_async]
161 pub fn update_progress(&self, progress: Option<u64>) -> Result<()> {
162 #[cfg(not(target_os = "android"))] {
163 Ok(())
164 }
165 #[cfg(target_os = "android")] {
166 {
167 let mut state = self.inner.lock_current_state();
168 state.progress = progress;
169 }
170
171 self.update_notification().await?;
172 Ok(())
173 }
174 }
175
176 #[maybe_async]
177 pub fn update_progress_max(&self, progress_max: Option<u64>) -> Result<()> {
178 #[cfg(not(target_os = "android"))] {
179 Ok(())
180 }
181 #[cfg(target_os = "android")] {
182 {
183 let mut state = self.inner.lock_current_state();
184 state.progress_max = progress_max;
185 }
186
187 self.update_notification().await?;
188 Ok(())
189 }
190 }
191
192 #[maybe_async]
193 pub fn update_title(&self, title: Option<&str>) -> Result<()> {
194 #[cfg(not(target_os = "android"))] {
195 Ok(())
196 }
197 #[cfg(target_os = "android")] {
198 {
199 let mut state = self.inner.lock_current_state();
200 state.title = title.map(|s| s.to_string());
201 }
202
203 self.update_notification().await?;
204 Ok(())
205 }
206 }
207
208 #[maybe_async]
209 pub fn update_text(&self, text: Option<&str>) -> Result<()> {
210 #[cfg(not(target_os = "android"))] {
211 Ok(())
212 }
213 #[cfg(target_os = "android")] {
214 {
215 let mut state = self.inner.lock_current_state();
216 state.text = text.map(|s| s.to_string());
217 }
218
219 self.update_notification().await?;
220 Ok(())
221 }
222 }
223
224 #[maybe_async]
225 pub fn update_sub_text(&self, sub_text: Option<&str>) -> Result<()> {
226 #[cfg(not(target_os = "android"))] {
227 Ok(())
228 }
229 #[cfg(target_os = "android")] {
230 {
231 let mut state = self.inner.lock_current_state();
232 state.sub_text = sub_text.map(|s| s.to_string());
233 };
234
235 self.update_notification().await?;
236 Ok(())
237 }
238 }
239
240 #[maybe_async]
241 pub fn update(
242 &self,
243 title: Option<&str>,
244 text: Option<&str>,
245 sub_text: Option<&str>,
246 progress: Option<u64>,
247 progress_max: Option<u64>,
248 ) -> Result<()> {
249
250 #[cfg(not(target_os = "android"))] {
251 Ok(())
252 }
253 #[cfg(target_os = "android")] {
254 {
255 let mut state = self.inner.lock_current_state();
256 *state = CurrentState {
257 title: title.map(|s| s.to_string()),
258 text: text.map(|s| s.to_string()),
259 sub_text: sub_text.map(|s| s.to_string()),
260 progress,
261 progress_max
262 };
263 }
264
265 self.update_notification().await?;
266 Ok(())
267 }
268 }
269
270 #[always_sync]
271 pub fn set_drop_behavior_to_complete(
272 &self,
273 title: Option<&str>,
274 text: Option<&str>,
275 sub_text: Option<&str>,
276 share_src: Option<&FileUri>
277 ) {
278
279 #[cfg(target_os = "android")] {
280 let title = title.map(|s| s.to_string());
281 let text = text.map(|s| s.to_string());
282 let sub_text = sub_text.map(|s| s.to_string());
283 let share_src = share_src.map(|s| s.clone());
284
285 *self.inner.lock_drop_behavior() = DropBehavior {
286 title: Some(Box::new(move || title)),
287 text: Some(Box::new(move || text)),
288 sub_text: Some(Box::new(move || sub_text)),
289 share_src: Some(Box::new(move || share_src)),
290 error: false,
291 };
292 }
293 }
294
295 #[always_sync]
296 pub fn set_drop_behavior_to_complete_with(
297 &self,
298 title: impl 'static + Send + FnOnce() -> Option<String>,
299 text: impl 'static + Send + FnOnce() -> Option<String>,
300 sub_text: impl 'static + Send + FnOnce() -> Option<String>,
301 share_src: impl 'static + Send + FnOnce() -> Option<FileUri>,
302 ) {
303
304 #[cfg(target_os = "android")] {
305 *self.inner.lock_drop_behavior() = DropBehavior {
306 title: Some(Box::new(title)),
307 text: Some(Box::new(text)),
308 sub_text: Some(Box::new(sub_text)),
309 share_src: Some(Box::new(share_src)),
310 error: false,
311 };
312 }
313 }
314
315 #[always_sync]
316 pub fn set_drop_behavior_to_fail(
317 &self,
318 title: Option<&str>,
319 text: Option<&str>,
320 sub_text: Option<&str>
321 ) {
322
323 #[cfg(target_os = "android")] {
324 let title = title.map(|s| s.to_string());
325 let text = text.map(|s| s.to_string());
326 let sub_text = sub_text.map(|s| s.to_string());
327
328 *self.inner.lock_drop_behavior() = DropBehavior {
329 title: Some(Box::new(move || title)),
330 text: Some(Box::new(move || text)),
331 sub_text: Some(Box::new(move || sub_text)),
332 share_src: None,
333 error: true,
334 };
335 }
336 }
337
338 #[always_sync]
339 pub fn set_drop_behavior_to_fail_with(
340 &self,
341 title: impl 'static + Send + FnOnce() -> Option<String>,
342 text: impl 'static + Send + FnOnce() -> Option<String>,
343 sub_text: impl 'static + Send + FnOnce() -> Option<String>,
344 ) {
345
346 #[cfg(target_os = "android")] {
347 *self.inner.lock_drop_behavior() = DropBehavior {
348 title: Some(Box::new(title)),
349 text: Some(Box::new(text)),
350 sub_text: Some(Box::new(sub_text)),
351 share_src: None,
352 error: true,
353 };
354 }
355 }
356
357 #[maybe_async]
358 pub fn complete(
359 self,
360 title: Option<&str>,
361 text: Option<&str>,
362 sub_text: Option<&str>,
363 share_src: Option<&FileUri>,
364 ) -> Result<()> {
365
366 #[cfg(not(target_os = "android"))] {
367 Ok(())
368 }
369 #[cfg(target_os = "android")] {
370 self.finish_notification(title, text, sub_text, share_src, false).await
371 }
372 }
373
374 #[maybe_async]
375 pub fn fail(
376 self,
377 title: Option<&str>,
378 text: Option<&str>,
379 sub_text: Option<&str>,
380 ) -> Result<()> {
381
382 #[cfg(not(target_os = "android"))] {
383 Ok(())
384 }
385 #[cfg(target_os = "android")] {
386 self.finish_notification(title, text, sub_text, None, true).await
387 }
388 }
389
390
391 #[cfg(target_os = "android")]
392 #[maybe_async]
393 fn finish_notification(
394 mut self,
395 title: Option<&str>,
396 text: Option<&str>,
397 sub_text: Option<&str>,
398 share_src: Option<&FileUri>,
399 error: bool
400 ) -> Result<()> {
401
402 self.impls().finish_progress_notification(
403 self.inner.id,
404 self.inner.icon,
405 title,
406 text,
407 sub_text,
408 share_src,
409 error,
410 ).await?;
411
412 self.inner.is_finished = true;
413 Ok(())
414 }
415
416 #[cfg(target_os = "android")]
417 #[maybe_async]
418 fn update_notification(&self) -> Result<()> {
419 if self.inner.update_throttler.check_and_mark() {
420 let state = self.inner.lock_current_state().clone();
421 let (progress, progress_max) = normalize_progress_and_max(state.progress, state.progress_max);
422
423 self.impls().update_progress_notification(
424 self.inner.id,
425 self.inner.icon,
426 state.title.as_deref(),
427 state.text.as_deref(),
428 state.sub_text.as_deref(),
429 progress,
430 progress_max,
431 ).await?;
432 }
433
434 Ok(())
435 }
436}
437
438
439#[cfg(target_os = "android")]
440struct Inner<R: tauri::Runtime> {
441 id: i32,
442 icon: ProgressNotificationIcon,
443 is_finished: bool,
444 drop_behavior: std::sync::Mutex<DropBehavior>,
445 current_state: std::sync::Mutex<CurrentState>,
446 update_throttler: Throttler,
447 handle: tauri::plugin::PluginHandle<R>,
448}
449
450#[cfg(target_os = "android")]
451struct DropBehavior {
452 title: Option<Box<dyn Send + 'static + FnOnce() -> Option<String>>>,
453 text: Option<Box<dyn Send + 'static + FnOnce() -> Option<String>>>,
454 sub_text: Option<Box<dyn Send + 'static + FnOnce() -> Option<String>>>,
455 share_src: Option<Box<dyn Send + 'static + FnOnce() -> Option<FileUri>>>,
456 error: bool,
457}
458
459#[cfg(target_os = "android")]
460#[derive(Clone)]
461struct CurrentState {
462 title: Option<String>,
463 text: Option<String>,
464 sub_text: Option<String>,
465 progress: Option<u64>,
466 progress_max: Option<u64>,
467}
468
469#[cfg(target_os = "android")]
470impl<R: tauri::Runtime> Inner<R> {
471
472 fn lock_drop_behavior<'a>(&'a self) -> std::sync::MutexGuard<'a, DropBehavior> {
473 self.drop_behavior.lock().unwrap_or_else(|e| e.into_inner())
474 }
475
476 fn lock_current_state<'a>(&'a self) -> std::sync::MutexGuard<'a, CurrentState> {
477 self.current_state.lock().unwrap_or_else(|e| e.into_inner())
478 }
479}
480
481#[cfg(target_os = "android")]
482impl<R: tauri::Runtime> Drop for Inner<R> {
483
484 fn drop(&mut self) {
485 if self.is_finished {
486 return
487 }
488
489 let handle = self.handle.clone();
490 let id = self.id;
491 let icon = self.icon;
492 let (error, title, text, sub_text, share_src) = {
493 let mut d = self.lock_drop_behavior();
494 (d.error, d.title.take(), d.text.take(), d.sub_text.take(), d.share_src.take())
495 };
496
497 tauri::async_runtime::spawn(async move {
498 let impls = impls::AsyncImpls { handle: &handle };
499 impls.finish_progress_notification(
500 id,
501 icon,
502 title.and_then(|f| f()).as_deref(),
503 text.and_then(|f| f()).as_deref(),
504 sub_text.and_then(|f| f()).as_deref(),
505 share_src.and_then(|f| f()).as_ref(),
506 error
507 ).await.ok();
508 });
509 }
510}
511
512#[cfg(target_os = "android")]
513struct Throttler {
514 next: std::sync::Mutex<std::time::Instant>,
515 interval: std::time::Duration,
516}
517
518#[cfg(target_os = "android")]
519impl Throttler {
520
521 pub fn with_delay(interval: std::time::Duration) -> Self {
522 Self {
523 next: std::sync::Mutex::new(std::time::Instant::now() + interval),
524 interval,
525 }
526 }
527
528 pub fn check_and_mark(&self) -> bool {
529 let mut next = self.next.lock().unwrap_or_else(|e| e.into_inner());
530 let now = std::time::Instant::now();
531
532 if now < *next {
533 return false
534 }
535
536 *next = now + self.interval;
537 true
538 }
539}
540
541#[cfg(target_os = "android")]
542fn normalize_progress_and_max(
543 progress: Option<u64>,
544 progress_max: Option<u64>,
545) -> (Option<i32>, Option<i32>) {
546
547 let Some((progress, progress_max)) = Option::zip(progress, progress_max) else {
548 return (None, None)
549 };
550
551 const PROGRESS_MAX: i32 = 100_000;
552
553 if progress_max == 0 {
554 return (Some(0), Some(0));
555 }
556
557 let ratio = progress as f64 / progress_max as f64;
558 let scaled_progress = (ratio * PROGRESS_MAX as f64) as i32;
559
560 (Some(i32::min(scaled_progress, PROGRESS_MAX)), Some(PROGRESS_MAX))
561}