tauri_plugin_android_fs/api/file_picker.rs
1use sync_async::sync_async;
2use crate::*;
3use super::*;
4
5
6/// API of file/dir picker.
7///
8/// # Examples
9/// ```no_run
10/// fn example(app: &tauri::AppHandle) {
11/// use tauri_plugin_android_fs::AndroidFsExt as _;
12///
13/// let api = app.android_fs();
14/// let file_picker = api.file_picker();
15/// }
16/// ```
17#[sync_async]
18pub struct FilePicker<'a, R: tauri::Runtime> {
19 #[cfg(target_os = "android")]
20 pub(crate) handle: &'a tauri::plugin::PluginHandle<R>,
21
22 #[cfg(not(target_os = "android"))]
23 #[allow(unused)]
24 pub(crate) handle: &'a std::marker::PhantomData<fn() -> R>,
25}
26
27#[cfg(target_os = "android")]
28#[sync_async(
29 use(if_sync) impls::SyncImpls as Impls;
30 use(if_async) impls::AsyncImpls as Impls;
31)]
32impl<'a, R: tauri::Runtime> FilePicker<'a, R> {
33
34 #[always_sync]
35 fn impls(&self) -> Impls<'_, R> {
36 Impls { handle: &self.handle }
37 }
38}
39
40#[sync_async(
41 use(if_async) api_async::{AndroidFs, FileOpener, PrivateStorage, PublicStorage};
42 use(if_sync) api_sync::{AndroidFs, FileOpener, PrivateStorage, PublicStorage};
43)]
44impl<'a, R: tauri::Runtime> FilePicker<'a, R> {
45
46 /// Opens a system file picker and returns a **read-write** URIs.
47 /// If no file is selected or the user cancels, an empty vec is returned.
48 ///
49 /// By default, returned URI is valid until the app or device is terminated.
50 /// If you want to persist it across app or device restarts, use [`FilePicker::persist_uri_permission`].
51 ///
52 /// This provides a standardized file explorer-style interface,
53 /// and also allows file selection from part of third-party apps or cloud storage.
54 ///
55 /// Removing the returned files is also supported in most cases,
56 /// but note that files provided by third-party apps may not be removable.
57 ///
58 /// # Args
59 /// - ***initial_location*** :
60 /// Indicate the initial location of dialog.
61 /// This URI works even without any permissions.
62 /// There is no need to use this if there is no special reason.
63 /// System will do its best to launch the dialog in the specified entry
64 /// if it's a directory, or the directory that contains the specified file if not.
65 /// If this is missing or failed to resolve the desired initial location, the initial location is system specific.
66 /// This must be a URI taken from following or it's derivative :
67 /// - [`PublicStorage::resolve_initial_location`]
68 /// - [`AndroidFs::resolve_root_initial_location`]
69 /// - [`FilePicker::pick_files`]
70 /// - [`FilePicker::pick_file`]
71 /// - [`FilePicker::pick_dir`]
72 /// - [`FilePicker::save_file`]
73 ///
74 /// - ***mime_types*** :
75 /// The MIME types of the file to be selected.
76 /// However, there is no guarantee that the returned file will match the specified types.
77 /// If left empty, all file types will be available (equivalent to `["*/*"]`).
78 ///
79 /// - ***local_only*** :
80 /// Indicates whether only entry located on the local device should be selectable, without requiring it to be downloaded from a remote service when opened.
81 ///
82 /// # Support
83 /// All Android version.
84 ///
85 /// # References
86 /// - <https://developer.android.com/reference/android/content/Intent#ACTION_OPEN_DOCUMENT>
87 #[maybe_async]
88 pub fn pick_files(
89 &self,
90 initial_location: Option<&FileUri>,
91 mime_types: &[&str],
92 local_only: bool,
93 ) -> Result<Vec<FileUri>> {
94
95 #[cfg(not(target_os = "android"))] {
96 Err(Error::NOT_ANDROID)
97 }
98 #[cfg(target_os = "android")] {
99 self.impls().show_pick_file_dialog(initial_location, mime_types, true, local_only).await
100 }
101 }
102
103 /// Opens a system file picker and returns a **read-write** URI.
104 /// If no file is selected or the user cancels, None is returned.
105 ///
106 /// By default, returned URI is valid until the app or device is terminated.
107 /// If you want to persist it across app or device restarts, use [`FilePicker::persist_uri_permission`].
108 ///
109 /// This provides a standardized file explorer-style interface,
110 /// and also allows file selection from part of third-party apps or cloud storage.
111 ///
112 /// Removing the returned files is also supported in most cases,
113 /// but note that files provided by third-party apps may not be removable.
114 ///
115 /// # Args
116 /// - ***initial_location*** :
117 /// Indicate the initial location of dialog.
118 /// This URI works even without any permissions.
119 /// There is no need to use this if there is no special reason.
120 /// System will do its best to launch the dialog in the specified entry
121 /// if it's a directory, or the directory that contains the specified file if not.
122 /// If this is missing or failed to resolve the desired initial location, the initial location is system specific.
123 /// This must be a URI taken from following or it's derivative :
124 /// - [`PublicStorage::resolve_initial_location`]
125 /// - [`AndroidFs::resolve_root_initial_location`]
126 /// - [`FilePicker::pick_files`]
127 /// - [`FilePicker::pick_file`]
128 /// - [`FilePicker::pick_dir`]
129 /// - [`FilePicker::save_file`]
130 ///
131 /// - ***mime_types*** :
132 /// The MIME types of the file to be selected.
133 /// However, there is no guarantee that the returned file will match the specified types.
134 /// If left empty, all file types will be available (equivalent to `["*/*"]`).
135 ///
136 /// - ***local_only*** :
137 /// Indicates whether only entry located on the local device should be selectable, without requiring it to be downloaded from a remote service when opened.
138 ///
139 /// # Support
140 /// All Android version.
141 ///
142 /// # References
143 /// - <https://developer.android.com/reference/android/content/Intent#ACTION_OPEN_DOCUMENT>
144 #[maybe_async]
145 pub fn pick_file(
146 &self,
147 initial_location: Option<&FileUri>,
148 mime_types: &[&str],
149 local_only: bool
150 ) -> Result<Option<FileUri>> {
151
152 #[cfg(not(target_os = "android"))] {
153 Err(Error::NOT_ANDROID)
154 }
155 #[cfg(target_os = "android")] {
156 self.impls().show_pick_file_dialog(initial_location, mime_types, false, local_only)
157 .await
158 .map(|mut i| i.pop())
159 }
160 }
161
162 /// Opens a media picker and returns a **readonly** URIs.
163 /// If no file is selected or the user cancels, an empty vec is returned.
164 ///
165 /// By default, returned URI is valid until the app or device is terminated.
166 /// If you want to persist it across app or device restarts, use [`FilePicker::persist_uri_permission`].
167 ///
168 /// This media picker provides a gallery,
169 /// sorted by date from newest to oldest.
170 ///
171 /// # Args
172 /// - ***target*** :
173 /// The media type of the file to be selected.
174 /// Images or videos, or both.
175 ///
176 /// - ***local_only*** :
177 /// Indicates whether only entry located on the local device should be selectable, without requiring it to be downloaded from a remote service when opened.
178 ///
179 /// # Note
180 /// The file obtained from this function cannot retrieve the correct file name using [`AndroidFs::get_name`].
181 /// Instead, it will be assigned a sequential number, such as `1000091523.png`.
182 /// And this is marked intended behavior, not a bug.
183 /// - <https://issuetracker.google.com/issues/268079113>
184 ///
185 /// # Support
186 /// This feature is available on devices that meet the following criteria:
187 /// - Running Android 11 (API level 30) or higher
188 /// - Receive changes to Modular System Components through Google System Updates
189 ///
190 /// Availability on a given device can be verified by calling [`FilePicker::is_visual_media_picker_available`].
191 /// If not supported, this function behaves the same as [`FilePicker::pick_files`].
192 ///
193 /// # References
194 /// - <https://developer.android.com/training/data-storage/shared/photopicker>
195 #[maybe_async]
196 pub fn pick_visual_medias(
197 &self,
198 target: VisualMediaTarget<'_>,
199 local_only: bool
200 ) -> Result<Vec<FileUri>> {
201
202 #[cfg(not(target_os = "android"))] {
203 Err(Error::NOT_ANDROID)
204 }
205 #[cfg(target_os = "android")] {
206 self.impls().show_pick_visual_media_dialog(target, true, local_only).await
207 }
208 }
209
210 /// Opens a media picker and returns a **readonly** URI.
211 /// If no file is selected or the user cancels, None is returned.
212 ///
213 /// By default, returned URI is valid until the app or device is terminated.
214 /// If you want to persist it across app or device restarts, use [`FilePicker::persist_uri_permission`].
215 ///
216 /// This media picker provides a gallery,
217 /// sorted by date from newest to oldest.
218 ///
219 /// # Args
220 /// - ***target*** :
221 /// The media type of the file to be selected.
222 /// Images or videos, or both.
223 ///
224 /// - ***local_only*** :
225 /// Indicates whether only entry located on the local device should be selectable, without requiring it to be downloaded from a remote service when opened.
226 ///
227 /// # Note
228 /// The file obtained from this function cannot retrieve the correct file name using [`AndroidFs::get_name`].
229 /// Instead, it will be assigned a sequential number, such as `1000091523.png`.
230 /// And this is marked intended behavior, not a bug.
231 /// - <https://issuetracker.google.com/issues/268079113>
232 ///
233 /// # Support
234 /// This feature is available on devices that meet the following criteria:
235 /// - Running Android 11 (API level 30) or higher
236 /// - Receive changes to Modular System Components through Google System Updates
237 ///
238 /// Availability on a given device can be verified by calling [`FilePicker::is_visual_media_picker_available`].
239 /// If not supported, this function behaves the same as [`FilePicker::pick_file`].
240 ///
241 /// # References
242 /// - <https://developer.android.com/training/data-storage/shared/photopicker>
243 #[maybe_async]
244 pub fn pick_visual_media(
245 &self,
246 target: VisualMediaTarget<'_>,
247 local_only: bool
248 ) -> Result<Option<FileUri>> {
249
250 #[cfg(not(target_os = "android"))] {
251 Err(Error::NOT_ANDROID)
252 }
253 #[cfg(target_os = "android")] {
254 self.impls().show_pick_visual_media_dialog(target, false, local_only)
255 .await
256 .map(|mut i| i.pop())
257 }
258 }
259
260 /// Opens a file picker and returns a **readonly** URIs.
261 /// If no file is selected or the user cancels, an empty vec is returned.
262 ///
263 /// Returned URI is valid until the app or device is terminated. Can not persist it.
264 ///
265 /// This works differently depending on the model and version.
266 /// Recent devices often have the similar behaviour as [`FilePicker::pick_visual_medias`] or [`FilePicker::pick_files`].
267 /// In older versions, third-party apps often handle request instead.
268 ///
269 /// # Args
270 /// - ***mime_types*** :
271 /// The MIME types of the file to be selected.
272 /// However, there is no guarantee that the returned file will match the specified types.
273 /// If left empty, all file types will be available (equivalent to `["*/*"]`).
274 ///
275 /// # Support
276 /// All Android version.
277 ///
278 /// # References
279 /// - <https://developer.android.com/reference/android/content/Intent#ACTION_GET_CONTENT>
280 #[maybe_async]
281 #[deprecated = "This may not support operations other than opening files."]
282 pub fn pick_contents(
283 &self,
284 mime_types: &[&str],
285 ) -> Result<Vec<FileUri>> {
286
287 #[cfg(not(target_os = "android"))] {
288 Err(Error::NOT_ANDROID)
289 }
290 #[cfg(target_os = "android")] {
291 self.impls().show_pick_content_dialog(mime_types, true).await
292 }
293 }
294
295 /// Opens a file picker and returns a **readonly** URI.
296 /// If no file is selected or the user cancels, None is returned.
297 ///
298 /// Returned URI is valid until the app or device is terminated. Can not persist it.
299 ///
300 /// This works differently depending on the model and version.
301 /// Recent devices often have the similar behaviour as [`FilePicker::pick_visual_media`] or [`FilePicker::pick_file`].
302 /// In older versions, third-party apps often handle request instead.
303 ///
304 /// # Args
305 /// - ***mime_types*** :
306 /// The MIME types of the file to be selected.
307 /// However, there is no guarantee that the returned file will match the specified types.
308 /// If left empty, all file types will be available (equivalent to `["*/*"]`).
309 ///
310 /// # Support
311 /// All Android version.
312 ///
313 /// # References
314 /// - <https://developer.android.com/reference/android/content/Intent#ACTION_GET_CONTENT>
315 #[maybe_async]
316 #[deprecated = "This may not support operations other than opening files."]
317 pub fn pick_content(
318 &self,
319 mime_types: &[&str],
320 ) -> Result<Option<FileUri>> {
321
322 #[cfg(not(target_os = "android"))] {
323 Err(Error::NOT_ANDROID)
324 }
325 #[cfg(target_os = "android")] {
326 self.impls().show_pick_content_dialog(mime_types, false)
327 .await
328 .map(|mut i| i.pop())
329 }
330 }
331
332 /// Opens a system directory picker, allowing the creation of a new directory or the selection of an existing one,
333 /// and returns a **read-write** directory URI.
334 /// App can fully manage entries within the returned directory.
335 /// If no directory is selected or the user cancels, `None` is returned.
336 ///
337 /// By default, returned URI is valid until the app or device is terminated.
338 /// If you want to persist it across app or device restarts, use [`FilePicker::persist_uri_permission`].
339 ///
340 /// This provides a standardized file explorer-style interface,
341 /// and also allows directory selection from part of third-party apps or cloud storage.
342 ///
343 /// # Args
344 /// - ***initial_location*** :
345 /// Indicate the initial location of dialog.
346 /// This URI works even without any permissions.
347 /// There is no need to use this if there is no special reason.
348 /// System will do its best to launch the dialog in the specified entry
349 /// if it's a directory, or the directory that contains the specified file if not.
350 /// If this is missing or failed to resolve the desired initial location, the initial location is system specific.
351 /// This must be a URI taken from following or it's derivative :
352 /// - [`PublicStorage::resolve_initial_location`]
353 /// - [`AndroidFs::resolve_root_initial_location`]
354 /// - [`FilePicker::pick_files`]
355 /// - [`FilePicker::pick_file`]
356 /// - [`FilePicker::pick_dir`]
357 /// - [`FilePicker::save_file`]
358 ///
359 /// - ***local_only*** :
360 /// Indicates whether only entry located on the local device should be selectable, without requiring it to be downloaded from a remote service when opened.
361 ///
362 /// # Support
363 /// All Android version.
364 ///
365 /// # References
366 /// - <https://developer.android.com/reference/android/content/Intent#ACTION_OPEN_DOCUMENT_TREE>
367 #[maybe_async]
368 pub fn pick_dir(
369 &self,
370 initial_location: Option<&FileUri>,
371 local_only: bool
372 ) -> Result<Option<FileUri>> {
373
374 #[cfg(not(target_os = "android"))] {
375 Err(Error::NOT_ANDROID)
376 }
377 #[cfg(target_os = "android")] {
378 self.impls().show_pick_dir_dialog(initial_location, local_only).await
379 }
380 }
381
382 /// Opens a system file saver and returns a **writeonly** URI.
383 /// The returned file may be a newly created file with no content,
384 /// or it may be an existing file with the requested MIME type.
385 /// If the user cancels, `None` is returned.
386 ///
387 /// By default, returned URI is valid until the app or device is terminated.
388 /// If you want to persist it across app or device restarts, use [`FilePicker::persist_uri_permission`].
389 ///
390 /// This provides a standardized file explorer-style interface,
391 /// and also allows file selection from part of third-party apps or cloud storage.
392 ///
393 /// Removing and reading the returned files is also supported in most cases,
394 /// but note that files provided by third-party apps may not.
395 ///
396 /// # Args
397 /// - ***initial_location*** :
398 /// Indicate the initial location of dialog.
399 /// This URI works even without any permissions.
400 /// There is no need to use this if there is no special reason.
401 /// System will do its best to launch the dialog in the specified entry
402 /// if it's a directory, or the directory that contains the specified file if not.
403 /// If this is missing or failed to resolve the desired initial location, the initial location is system specific.
404 /// This must be a URI taken from following or it's derivative :
405 /// - [`PublicStorage::resolve_initial_location`]
406 /// - [`AndroidFs::resolve_root_initial_location`]
407 /// - [`FilePicker::pick_files`]
408 /// - [`FilePicker::pick_file`]
409 /// - [`FilePicker::pick_dir`]
410 /// - [`FilePicker::save_file`]
411 ///
412 /// - ***initial_file_name*** :
413 /// An initial file name.
414 /// The user may change this value before creating the file.
415 /// If no extension is present,
416 /// the system may infer one from ***mime_type*** and may append it to the file name.
417 /// But this append-extension operation depends on the model and version.
418 ///
419 /// - ***mime_type*** :
420 /// The MIME type of the file to be saved.
421 /// If this is None, MIME type is inferred from the extension of ***initial_file_name*** (not file name by user input)
422 /// and if that fails, `application/octet-stream` is used.
423 ///
424 /// - ***local_only*** :
425 /// Indicates whether only entry located on the local device should be selectable.
426 ///
427 /// # Support
428 /// All Android version.
429 ///
430 /// # References
431 /// - <https://developer.android.com/reference/android/content/Intent#ACTION_CREATE_DOCUMENT>
432 #[maybe_async]
433 pub fn save_file(
434 &self,
435 initial_location: Option<&FileUri>,
436 initial_file_name: impl AsRef<str>,
437 mime_type: Option<&str>,
438 local_only: bool
439 ) -> Result<Option<FileUri>> {
440
441 #[cfg(not(target_os = "android"))] {
442 Err(Error::NOT_ANDROID)
443 }
444 #[cfg(target_os = "android")] {
445 self.impls().show_save_file_dialog(initial_location, initial_file_name, mime_type, local_only).await
446 }
447 }
448
449 /// Verify whether [`FilePicker::pick_visual_medias`] is available on a given device.
450 ///
451 /// # Support
452 /// All Android version.
453 #[maybe_async]
454 pub fn is_visual_media_picker_available(&self) -> Result<bool> {
455 #[cfg(not(target_os = "android"))] {
456 Err(Error::NOT_ANDROID)
457 }
458 #[cfg(target_os = "android")] {
459 self.impls().is_visual_media_picker_available().await
460 }
461 }
462
463 /// Check a URI permission granted by the file picker.
464 /// Returns false if there are no permissions.
465 ///
466 /// # Args
467 /// - **uri** :
468 /// URI of the target file or directory.
469 ///
470 /// - **permission** :
471 /// The permission you want to check.
472 ///
473 ///
474 /// # Support
475 /// All Android version.
476 #[maybe_async]
477 pub fn check_uri_permission(
478 &self,
479 uri: &FileUri,
480 permission: UriPermission
481 ) -> Result<bool> {
482
483 #[cfg(not(target_os = "android"))] {
484 Err(Error::NOT_ANDROID)
485 }
486 #[cfg(target_os = "android")] {
487 self.impls().check_picker_uri_permission(uri, permission).await
488 }
489 }
490
491 /// Take persistent permission to access the file, directory and its descendants.
492 /// This is a prolongation of an already acquired permission, not the acquisition of a new one.
493 ///
494 /// This works by just calling, without displaying any confirmation to the user.
495 ///
496 /// Note that [there is a limit to the total number of URI that can be made persistent by this function.](https://stackoverflow.com/questions/71099575/should-i-release-persistableuripermission-when-a-new-storage-location-is-chosen/71100621#71100621)
497 /// Therefore, it is recommended to relinquish the unnecessary persisted URI by [`FilePicker::release_persisted_uri_permission`] or [`FilePicker::release_all_persisted_uri_permissions`].
498 /// Persisted permissions may be relinquished by other apps, user, or by moving/removing entries.
499 /// So check by [`FilePicker::check_persisted_uri_permission`].
500 /// And you can retrieve the list of persisted uris using [`FilePicker::get_all_persisted_uri_permissions`].
501 ///
502 /// # Args
503 /// - **uri** :
504 /// URI of the target file or directory.
505 /// This must be a URI taken from following :
506 /// - [`FilePicker::pick_files`]
507 /// - [`FilePicker::pick_file`]
508 /// - [`FilePicker::pick_visual_medias`]
509 /// - [`FilePicker::pick_visual_media`]
510 /// - [`FilePicker::pick_dir`]
511 /// - [`FilePicker::save_file`]
512 /// - [`AndroidFs::resolve_file_uri`], [`AndroidFs::resolve_dir_uri`], [`AndroidFs::read_dir`], [`AndroidFs::create_new_file`], [`AndroidFs::create_dir_all`] :
513 /// If use URI from thoese fucntions, the permissions of the origin directory URI is persisted, not a entry iteself by this function.
514 /// Because the permissions and validity period of the descendant entry URIs depend on the origin directory.
515 ///
516 /// # Support
517 /// All Android version.
518 #[maybe_async]
519 pub fn persist_uri_permission(&self, uri: &FileUri) -> Result<()> {
520 #[cfg(not(target_os = "android"))] {
521 Err(Error::NOT_ANDROID)
522 }
523 #[cfg(target_os = "android")] {
524 self.impls().persist_picker_uri_permission(uri).await
525 }
526 }
527
528 /// Check a persisted URI permission grant by [`FilePicker::persist_uri_permission`].
529 /// Returns false if there are only non-persistent permissions or no permissions.
530 ///
531 /// # Args
532 /// - **uri** :
533 /// URI of the target file or directory.
534 /// This must be a URI taken from following :
535 /// - [`FilePicker::pick_files`]
536 /// - [`FilePicker::pick_file`]
537 /// - [`FilePicker::pick_visual_medias`]
538 /// - [`FilePicker::pick_visual_media`]
539 /// - [`FilePicker::pick_dir`]
540 /// - [`FilePicker::save_file`]
541 /// - [`AndroidFs::resolve_file_uri`], [`AndroidFs::resolve_dir_uri`], [`AndroidFs::read_dir`], [`AndroidFs::create_new_file`], [`AndroidFs::create_dir_all`] :
542 /// If use URI from those functions, the permissions of the origin directory URI is checked, not a entry iteself by this function.
543 /// Because the permissions and validity period of the descendant entry URIs depend on the origin directory.
544 ///
545 /// - **permission** :
546 /// The permission you want to check.
547 ///
548 /// # Support
549 /// All Android version.
550 #[maybe_async]
551 pub fn check_persisted_uri_permission(
552 &self,
553 uri: &FileUri,
554 permission: UriPermission
555 ) -> Result<bool> {
556
557 #[cfg(not(target_os = "android"))] {
558 Err(Error::NOT_ANDROID)
559 }
560 #[cfg(target_os = "android")] {
561 self.impls().check_persisted_picker_uri_permission(uri, permission).await
562 }
563 }
564
565 /// Return list of all persisted URIs that have been persisted by [`FilePicker::persist_uri_permission`] and currently valid.
566 ///
567 /// # Support
568 /// All Android version.
569 #[maybe_async]
570 pub fn get_all_persisted_uri_permissions(&self) -> Result<Vec<PersistedUriPermissionState>> {
571 #[cfg(not(target_os = "android"))] {
572 Err(Error::NOT_ANDROID)
573 }
574 #[cfg(target_os = "android")] {
575 self.impls()
576 .get_all_persisted_picker_uri_permissions().await
577 .map(|v| v.collect())
578 }
579 }
580
581 /// Relinquish a persisted URI permission grant by [`FilePicker::persist_uri_permission`].
582 /// Non-persistent permissions are not released.
583 ///
584 /// Returns true if a persisted permission exists for the specified URI and was successfully released;
585 /// otherwise, returns false if no persisted permission existed in the first place.
586 ///
587 /// # Args
588 /// - ***uri*** :
589 /// URI of the target file or directory.
590 ///
591 /// # Support
592 /// All Android version.
593 #[maybe_async]
594 pub fn release_persisted_uri_permission(&self, uri: &FileUri) -> Result<bool> {
595 #[cfg(not(target_os = "android"))] {
596 Err(Error::NOT_ANDROID)
597 }
598 #[cfg(target_os = "android")] {
599 self.impls().release_persisted_picker_uri_permission(uri).await
600 }
601 }
602
603 /// Relinquish a all persisted uri permission grants by [`FilePicker::persist_uri_permission`].
604 /// Non-persistent permissions are not released.
605 ///
606 /// # Support
607 /// All Android version.
608 #[maybe_async]
609 pub fn release_all_persisted_uri_permissions(&self) -> Result<()> {
610 #[cfg(not(target_os = "android"))] {
611 Err(Error::NOT_ANDROID)
612 }
613 #[cfg(target_os = "android")] {
614 self.impls().release_all_persisted_picker_uri_permissions().await
615 }
616 }
617}