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