Skip to main content

tauri_plugin_android_fs/api/
progress_notification_guard.rs

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