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_fail(
293        &self,
294        title: Option<&str>,
295        text: Option<&str>,
296        sub_text: Option<&str>
297    ) {
298
299        #[cfg(target_os = "android")] {
300            let title = title.map(|s| s.to_string());
301            let text = text.map(|s| s.to_string());
302            let sub_text = sub_text.map(|s| s.to_string());
303
304            *self.inner.lock_drop_behavior() = DropBehavior { 
305                title: Some(Box::new(move |_| title)),
306                text: Some(Box::new(move |_| text)),
307                sub_text: Some(Box::new(move |_| sub_text)),
308                error: true,
309            };
310        }
311    }
312
313    #[always_sync]
314    #[allow(dead_code)]
315    #[cfg(target_os = "android")]
316    pub(crate) fn set_drop_behavior_to_fail_with(
317        &self,
318        title: impl 'static + Send + Sync + FnOnce(&CurrentState) -> Option<String>,
319        text: impl 'static + Send + Sync + FnOnce(&CurrentState) -> Option<String>,
320        sub_text: impl 'static + Send + Sync + FnOnce(&CurrentState) -> Option<String>,
321    ) {
322        
323        #[cfg(target_os = "android")] {
324            *self.inner.lock_drop_behavior() = DropBehavior { 
325                title: Some(Box::new(title)),
326                text: Some(Box::new(text)),
327                sub_text: Some(Box::new(sub_text)),
328                error: true,
329            };
330        }
331    }
332
333    #[maybe_async]
334    pub fn complete(
335        self,
336        title: Option<&str>,
337        text: Option<&str>,
338        sub_text: Option<&str>,
339    ) -> Result<()> {
340
341        #[cfg(not(target_os = "android"))] {
342            Ok(())
343        }
344        #[cfg(target_os = "android")] {
345            self.finish_notification(title, text, sub_text, false).await
346        }
347    }
348
349    #[maybe_async]
350    pub fn fail(
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, true).await
362        }
363    }
364    
365    
366    #[cfg(target_os = "android")] 
367    #[maybe_async]
368    fn finish_notification(
369        mut self,
370        title: Option<&str>, 
371        text: Option<&str>,
372        sub_text: Option<&str>,
373        error: bool
374    ) -> Result<()> {
375
376        self.impls().finish_progress_notification(
377            self.inner.id, 
378            self.inner.icon,
379            title,
380            text,
381            sub_text,
382            error
383        ).await?;
384            
385        self.inner.is_finished = true;
386        Ok(())
387    }
388
389    #[cfg(target_os = "android")] 
390    #[maybe_async]
391    fn update_notification(&self) -> Result<()> {
392        if self.inner.update_throttler.check_and_mark() {
393            let state = self.inner.lock_current_state().clone();
394            let (progress, progress_max) = normalize_progress_and_max(state.progress, state.progress_max);
395            
396            self.impls().update_progress_notification(
397                self.inner.id,
398                self.inner.icon, 
399                state.title.as_deref(),
400                state.text.as_deref(), 
401                state.sub_text.as_deref(), 
402                progress,
403                progress_max,
404            ).await?;
405        }
406
407        Ok(())
408    }
409}
410
411
412#[cfg(target_os = "android")]
413struct Inner<R: tauri::Runtime> {
414    id: i32,
415    icon: ProgressNotificationIcon,
416    is_finished: bool,
417    drop_behavior: std::sync::Mutex<DropBehavior>,
418    current_state: std::sync::Mutex<CurrentState>,
419    update_throttler: Throttler,
420    handle: tauri::plugin::PluginHandle<R>,
421}
422
423#[cfg(target_os = "android")]
424struct DropBehavior {
425    title: Option<Box<dyn Sync + Send + 'static + FnOnce(&CurrentState) -> Option<String>>>,
426    text: Option<Box<dyn Sync + Send + 'static + FnOnce(&CurrentState) -> Option<String>>>,
427    sub_text: Option<Box<dyn Sync + Send + 'static + FnOnce(&CurrentState) -> Option<String>>>,
428    error: bool,
429}
430
431#[cfg(target_os = "android")]
432#[derive(Clone)]
433pub(crate) struct CurrentState {
434    pub title: Option<String>,
435    pub text: Option<String>,
436    pub sub_text: Option<String>,
437    pub progress: Option<u64>,
438    pub progress_max: Option<u64>,
439}
440
441#[cfg(target_os = "android")]
442impl<R: tauri::Runtime> Inner<R> {
443
444    fn lock_drop_behavior<'a>(&'a self) -> std::sync::MutexGuard<'a, DropBehavior> {
445        self.drop_behavior.lock().unwrap_or_else(|e| e.into_inner())
446    }
447
448    fn lock_current_state<'a>(&'a self) -> std::sync::MutexGuard<'a, CurrentState> {
449        self.current_state.lock().unwrap_or_else(|e| e.into_inner())
450    }
451}
452
453#[cfg(target_os = "android")]
454impl<R: tauri::Runtime> Drop for Inner<R> {
455
456    fn drop(&mut self) {
457        if self.is_finished {
458            return
459        }
460
461        let handle = self.handle.clone();
462        let id = self.id;
463        let icon = self.icon;
464        let current_state = self.lock_current_state().clone();
465        let (error, title, text, sub_text) = {
466            let mut d = self.lock_drop_behavior();
467            (d.error, d.title.take(), d.text.take(), d.sub_text.take())
468        };
469
470        tauri::async_runtime::spawn(async move {
471            let impls = impls::AsyncImpls { handle: &handle };
472            impls.finish_progress_notification(
473                id, 
474                icon,
475                title.and_then(|f| f(&current_state)).as_deref(),
476                text.and_then(|f| f(&current_state)).as_deref(),
477                sub_text.and_then(|f| f(&current_state)).as_deref(),
478                error
479            ).await.ok();
480        });
481    }
482}
483
484#[cfg(target_os = "android")]
485struct Throttler {
486    next: std::sync::Mutex<std::time::Instant>,
487    interval: std::time::Duration,
488}
489
490#[cfg(target_os = "android")]
491impl Throttler {
492
493    pub fn with_delay(interval: std::time::Duration) -> Self {
494        Self {
495            next: std::sync::Mutex::new(std::time::Instant::now() + interval),
496            interval,
497        }
498    }
499
500    pub fn check_and_mark(&self) -> bool {
501        let mut next = self.next.lock().unwrap_or_else(|e| e.into_inner());
502        let now = std::time::Instant::now();
503        
504        if now < *next {
505            return false
506        }
507
508        *next = now + self.interval;
509        true
510    }
511}
512
513#[cfg(target_os = "android")]
514fn normalize_progress_and_max(
515    progress: Option<u64>,
516    progress_max: Option<u64>,
517) -> (Option<i32>, Option<i32>) {
518
519    let Some((progress, progress_max)) = Option::zip(progress, progress_max) else {
520        return (None, None)
521    };
522    
523    const PROGRESS_MAX: i32 = 100_000;
524
525    if progress_max == 0 {
526        return (Some(0), Some(0)); 
527    }
528
529    let ratio = progress as f64 / progress_max as f64;
530    let scaled_progress = (ratio * PROGRESS_MAX as f64) as i32;
531
532    (Some(i32::min(scaled_progress, PROGRESS_MAX)), Some(PROGRESS_MAX))
533}