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                    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}