tauri_plugin_android_fs/api/file_picker.rs
1use crate::*;
2
3
4/// API of file/dir picker.
5///
6/// # Examples
7/// ```no_run
8/// fn example(app: &tauri::AppHandle) {
9/// use tauri_plugin_android_fs::AndroidFsExt as _;
10///
11/// let api = app.android_fs();
12/// let file_picker = api.file_picker();
13/// }
14/// ```
15pub struct FilePicker<'a, R: tauri::Runtime>(
16 #[allow(unused)]
17 pub(crate) &'a AndroidFs<R>
18);
19
20impl<'a, R: tauri::Runtime> FilePicker<'a, R> {
21
22 /// Opens a system file picker and returns a **read-write** URIs.
23 /// If no file is selected or the user cancels, an empty vec is returned.
24 ///
25 /// By default, returned URI is valid until the app or device is terminated.
26 /// If you want to persist it across app restarts, use [`AndroidFs::take_persistable_uri_permission`].
27 ///
28 /// This provides a standardized file explorer-style interface,
29 /// and also allows file selection from part of third-party apps or cloud storage.
30 ///
31 /// Removing the returned files is also supported in most cases,
32 /// but note that files provided by third-party apps may not be removable.
33 ///
34 /// # Args
35 /// - ***initial_location*** :
36 /// Indicate the initial location of dialog.
37 /// This URI works even without any permissions.
38 /// There is no need to use this if there is no special reason.
39 /// System will do its best to launch the dialog in the specified entry
40 /// if it's a directory, or the directory that contains the specified file if not.
41 /// If this is missing or failed to resolve the desired initial location, the initial location is system specific.
42 /// This must be a URI taken from following :
43 /// - [`PublicStorage::resolve_initial_location`]
44 /// - [`PublicStorage::resolve_initial_location_top`]
45 /// - [`AndroidFs::try_resolve_file_uri`]
46 /// - [`AndroidFs::try_resolve_dir_uri`]
47 /// - [`AndroidFs::resolve_uri`]
48 /// - [`AndroidFs::read_dir`]
49 /// - [`AndroidFs::create_new_file`]
50 /// - [`AndroidFs::create_dir_all`]
51 /// - [`AndroidFs::rename`]
52 /// - [`FilePicker::pick_files`]
53 /// - [`FilePicker::pick_file`]
54 /// - [`FilePicker::pick_dir`]
55 /// - [`FilePicker::save_file`]
56 ///
57 /// - ***mime_types*** :
58 /// The MIME types of the file to be selected.
59 /// However, there is no guarantee that the returned file will match the specified types.
60 /// If left empty, all file types will be available (equivalent to `["*/*"]`).
61 ///
62 /// # Support
63 /// All Android version.
64 ///
65 /// # References
66 /// <https://developer.android.com/reference/android/content/Intent#ACTION_OPEN_DOCUMENT>
67 pub fn pick_files(
68 &self,
69 initial_location: Option<&FileUri>,
70 mime_types: &[&str],
71 ) -> crate::Result<Vec<FileUri>> {
72
73 self.inner_pick_files(initial_location, mime_types, true)
74 }
75
76 /// Opens a system file picker and returns a **read-write** URI.
77 /// If no file is selected or the user cancels, None is returned.
78 ///
79 /// By default, returned URI is valid until the app or device is terminated.
80 /// If you want to persist it across app restarts, use [`AndroidFs::take_persistable_uri_permission`].
81 ///
82 /// This provides a standardized file explorer-style interface,
83 /// and also allows file selection from part of third-party apps or cloud storage.
84 ///
85 /// Removing the returned files is also supported in most cases,
86 /// but note that files provided by third-party apps may not be removable.
87 ///
88 /// # Args
89 /// - ***initial_location*** :
90 /// Indicate the initial location of dialog.
91 /// This URI works even without any permissions.
92 /// There is no need to use this if there is no special reason.
93 /// System will do its best to launch the dialog in the specified entry
94 /// if it's a directory, or the directory that contains the specified file if not.
95 /// If this is missing or failed to resolve the desired initial location, the initial location is system specific.
96 /// This must be a URI taken from following :
97 /// - [`PublicStorage::resolve_initial_location`]
98 /// - [`PublicStorage::resolve_initial_location_top`]
99 /// - [`AndroidFs::try_resolve_file_uri`]
100 /// - [`AndroidFs::try_resolve_dir_uri`]
101 /// - [`AndroidFs::resolve_uri`]
102 /// - [`AndroidFs::read_dir`]
103 /// - [`AndroidFs::create_new_file`]
104 /// - [`AndroidFs::create_dir_all`]
105 /// - [`AndroidFs::rename`]
106 /// - [`FilePicker::pick_files`]
107 /// - [`FilePicker::pick_file`]
108 /// - [`FilePicker::pick_dir`]
109 /// - [`FilePicker::save_file`]
110 ///
111 /// - ***mime_types*** :
112 /// The MIME types of the file to be selected.
113 /// However, there is no guarantee that the returned file will match the specified types.
114 /// If left empty, all file types will be available (equivalent to `["*/*"]`).
115 ///
116 /// # Support
117 /// All Android version.
118 ///
119 /// # References
120 /// <https://developer.android.com/reference/android/content/Intent#ACTION_OPEN_DOCUMENT>
121 pub fn pick_file(
122 &self,
123 initial_location: Option<&FileUri>,
124 mime_types: &[&str],
125 ) -> crate::Result<Option<FileUri>> {
126
127 self.inner_pick_files(initial_location, mime_types, false).map(|mut f| f.pop())
128 }
129
130 /// Opens a media picker and returns a **readonly** URIs.
131 /// If no file is selected or the user cancels, an empty vec is returned.
132 ///
133 /// By default, returned URI is valid until the app or device is terminated.
134 /// If you want to persist it across app restarts, use [`AndroidFs::take_persistable_uri_permission`].
135 ///
136 /// This media picker provides a gallery,
137 /// sorted by date from newest to oldest.
138 ///
139 /// # Args
140 /// - ***target*** :
141 /// The media type of the file to be selected.
142 /// Images or videos, or both.
143 ///
144 /// # Note
145 /// The file obtained from this function cannot retrieve the correct file name using [`AndroidFs::get_name`].
146 /// Instead, it will be assigned a sequential number, such as `1000091523.png`.
147 /// And this is marked intended behavior, not a bug.
148 /// - <https://issuetracker.google.com/issues/268079113>
149 ///
150 /// # Support
151 /// This feature is available on devices that meet the following criteria:
152 /// - Running Android 11 (API level 30) or higher
153 /// - Receive changes to Modular System Components through Google System Updates
154 ///
155 /// Availability on a given device can be verified by calling [`FilePicker::is_visual_media_picker_available`].
156 /// If not supported, this function behaves the same as [`FilePicker::pick_files`].
157 ///
158 /// # References
159 /// <https://developer.android.com/training/data-storage/shared/photopicker>
160 pub fn pick_visual_medias(
161 &self,
162 target: VisualMediaTarget<'_>,
163 ) -> crate::Result<Vec<FileUri>> {
164
165 self.inner_pick_visual_medias(target, true)
166 }
167
168 /// Opens a media picker and returns a **readonly** URI.
169 /// If no file is selected or the user cancels, None is returned.
170 ///
171 /// By default, returned URI is valid until the app or device is terminated.
172 /// If you want to persist it across app restarts, use [`AndroidFs::take_persistable_uri_permission`].
173 ///
174 /// This media picker provides a gallery,
175 /// sorted by date from newest to oldest.
176 ///
177 /// # Args
178 /// - ***target*** :
179 /// The media type of the file to be selected.
180 /// Images or videos, or both.
181 ///
182 /// # Note
183 /// The file obtained from this function cannot retrieve the correct file name using [`AndroidFs::get_name`].
184 /// Instead, it will be assigned a sequential number, such as `1000091523.png`.
185 /// And this is marked intended behavior, not a bug.
186 /// - <https://issuetracker.google.com/issues/268079113>
187 ///
188 /// # Support
189 /// This feature is available on devices that meet the following criteria:
190 /// - Running Android 11 (API level 30) or higher
191 /// - Receive changes to Modular System Components through Google System Updates
192 ///
193 /// Availability on a given device can be verified by calling [`FilePicker::is_visual_media_picker_available`].
194 /// If not supported, this function behaves the same as [`FilePicker::pick_file`].
195 ///
196 /// # References
197 /// <https://developer.android.com/training/data-storage/shared/photopicker>
198 pub fn pick_visual_media(
199 &self,
200 target: VisualMediaTarget<'_>,
201 ) -> crate::Result<Option<FileUri>> {
202
203 self.inner_pick_visual_medias(target, false).map(|mut f| f.pop())
204 }
205
206 /// Opens a file picker and returns a **readonly** URIs.
207 /// If no file is selected or the user cancels, an empty vec is returned.
208 ///
209 /// Returned URI is valid until the app or device is terminated. Can not persist it.
210 ///
211 /// This works differently depending on the model and version.
212 /// Recent devices often have the similar behaviour as [`FilePicker::pick_visual_medias`] or [`FilePicker::pick_files`].
213 /// In older versions, third-party apps often handle request instead.
214 ///
215 /// # Args
216 /// - ***mime_types*** :
217 /// The MIME types of the file to be selected.
218 /// However, there is no guarantee that the returned file will match the specified types.
219 /// If left empty, all file types will be available (equivalent to `["*/*"]`).
220 ///
221 /// # Support
222 /// All Android version.
223 ///
224 /// # References
225 /// <https://developer.android.com/reference/android/content/Intent#ACTION_GET_CONTENT>
226 pub fn pick_contents(
227 &self,
228 mime_types: &[&str],
229 ) -> crate::Result<Vec<FileUri>> {
230
231 self.inner_pick_contents(mime_types, true)
232 }
233
234 /// Opens a file picker and returns a **readonly** URI.
235 /// If no file is selected or the user cancels, None is returned.
236 ///
237 /// Returned URI is valid until the app or device is terminated. Can not persist it.
238 ///
239 /// This works differently depending on the model and version.
240 /// Recent devices often have the similar behaviour as [`FilePicker::pick_visual_media`] or [`FilePicker::pick_file`].
241 /// In older versions, third-party apps often handle request instead.
242 ///
243 /// # Args
244 /// - ***mime_types*** :
245 /// The MIME types of the file to be selected.
246 /// However, there is no guarantee that the returned file will match the specified types.
247 /// If left empty, all file types will be available (equivalent to `["*/*"]`).
248 ///
249 /// # Support
250 /// All Android version.
251 ///
252 /// # References
253 /// <https://developer.android.com/reference/android/content/Intent#ACTION_GET_CONTENT>
254 pub fn pick_content(
255 &self,
256 mime_types: &[&str],
257 ) -> crate::Result<Option<FileUri>> {
258
259 self.inner_pick_contents(mime_types, false).map(|mut f| f.pop())
260 }
261
262 /// Opens a system directory picker, allowing the creation of a new directory or the selection of an existing one,
263 /// and returns a **read-write** directory URI.
264 /// App can fully manage entries within the returned directory.
265 /// If no directory is selected or the user cancels, `None` is returned.
266 ///
267 /// By default, returned URI is valid until the app or device is terminated.
268 /// If you want to persist it across app restarts, use [`AndroidFs::take_persistable_uri_permission`].
269 ///
270 /// This provides a standardized file explorer-style interface,
271 /// and also allows directory selection from part of third-party apps or cloud storage.
272 ///
273 /// # Args
274 /// - ***initial_location*** :
275 /// Indicate the initial location of dialog.
276 /// This URI works even without any permissions.
277 /// There is no need to use this if there is no special reason.
278 /// System will do its best to launch the dialog in the specified entry
279 /// if it's a directory, or the directory that contains the specified file if not.
280 /// If this is missing or failed to resolve the desired initial location, the initial location is system specific.
281 /// This must be a URI taken from following :
282 /// - [`PublicStorage::resolve_initial_location`]
283 /// - [`PublicStorage::resolve_initial_location_top`]
284 /// - [`AndroidFs::try_resolve_file_uri`]
285 /// - [`AndroidFs::try_resolve_dir_uri`]
286 /// - [`AndroidFs::resolve_uri`]
287 /// - [`AndroidFs::read_dir`]
288 /// - [`AndroidFs::create_new_file`]
289 /// - [`AndroidFs::create_dir_all`]
290 /// - [`AndroidFs::rename`]
291 /// - [`FilePicker::pick_files`]
292 /// - [`FilePicker::pick_file`]
293 /// - [`FilePicker::pick_dir`]
294 /// - [`FilePicker::save_file`]
295 ///
296 /// # Support
297 /// All Android version.
298 ///
299 /// # References
300 /// <https://developer.android.com/reference/android/content/Intent#ACTION_OPEN_DOCUMENT_TREE>
301 pub fn pick_dir(
302 &self,
303 initial_location: Option<&FileUri>,
304 ) -> crate::Result<Option<FileUri>> {
305
306 on_android!({
307 impl_se!(struct Req<'a> { initial_location: Option<&'a FileUri> });
308 impl_de!(struct Res { uri: Option<FileUri> });
309
310 let _guard = self.0.intent_lock.lock();
311 self.0.api
312 .run_mobile_plugin::<Res>("showManageDirDialog", Req { initial_location })
313 .map(|v| v.uri)
314 .map_err(Into::into)
315 })
316 }
317
318 /// Opens a system file saver and returns a **writeonly** URI.
319 /// The returned file may be a newly created file with no content,
320 /// or it may be an existing file with the requested MIME type.
321 /// If the user cancels, `None` is returned.
322 ///
323 /// By default, returned URI is valid until the app or device is terminated.
324 /// If you want to persist it across app restarts, use [`AndroidFs::take_persistable_uri_permission`].
325 ///
326 /// This provides a standardized file explorer-style interface,
327 /// and also allows file selection from part of third-party apps or cloud storage.
328 ///
329 /// Removing and reading the returned files is also supported in most cases,
330 /// but note that files provided by third-party apps may not.
331 ///
332 /// # Args
333 /// - ***initial_location*** :
334 /// Indicate the initial location of dialog.
335 /// This URI works even without any permissions.
336 /// There is no need to use this if there is no special reason.
337 /// System will do its best to launch the dialog in the specified entry
338 /// if it's a directory, or the directory that contains the specified file if not.
339 /// If this is missing or failed to resolve the desired initial location, the initial location is system specific.
340 /// This must be a URI taken from following :
341 /// - [`PublicStorage::resolve_initial_location`]
342 /// - [`PublicStorage::resolve_initial_location_top`]
343 /// - [`AndroidFs::try_resolve_file_uri`]
344 /// - [`AndroidFs::try_resolve_dir_uri`]
345 /// - [`AndroidFs::resolve_uri`]
346 /// - [`AndroidFs::read_dir`]
347 /// - [`AndroidFs::create_new_file`]
348 /// - [`AndroidFs::create_dir_all`]
349 /// - [`AndroidFs::rename`]
350 /// - [`FilePicker::pick_files`]
351 /// - [`FilePicker::pick_file`]
352 /// - [`FilePicker::pick_dir`]
353 /// - [`FilePicker::save_file`]
354 ///
355 /// - ***initial_file_name*** :
356 /// An initial file name.
357 /// The user may change this value before creating the file.
358 /// If no extension is present,
359 /// the system may infer one from ***mime_type*** and may append it to the file name.
360 /// But this append-extension operation depends on the model and version.
361 ///
362 /// - ***mime_type*** :
363 /// The MIME type of the file to be saved.
364 /// If this is None, MIME type is inferred from the extension of ***initial_file_name*** (not file name by user input)
365 /// and if that fails, `application/octet-stream` is used.
366 ///
367 /// # Support
368 /// All Android version.
369 ///
370 /// # References
371 /// <https://developer.android.com/reference/android/content/Intent#ACTION_CREATE_DOCUMENT>
372 pub fn save_file(
373 &self,
374 initial_location: Option<&FileUri>,
375 initial_file_name: impl AsRef<str>,
376 mime_type: Option<&str>,
377 ) -> crate::Result<Option<FileUri>> {
378
379 on_android!({
380 impl_se!(struct Req<'a> {
381 initial_file_name: &'a str,
382 mime_type: Option<&'a str>,
383 initial_location: Option<&'a FileUri>
384 });
385 impl_de!(struct Res { uri: Option<FileUri> });
386
387 let initial_file_name = initial_file_name.as_ref();
388
389 let _guard = self.0.intent_lock.lock();
390 self.0.api
391 .run_mobile_plugin::<Res>("showSaveFileDialog", Req { initial_file_name, mime_type, initial_location })
392 .map(|v| v.uri)
393 .map_err(Into::into)
394 })
395 }
396
397 /// Verify whether [`FilePicker::pick_visual_medias`] is available on a given device.
398 ///
399 /// # Support
400 /// All Android version.
401 pub fn is_visual_media_picker_available(&self) -> crate::Result<bool> {
402 on_android!({
403 impl_de!(struct Res { value: bool });
404
405 self.0.api
406 .run_mobile_plugin::<Res>("isVisualMediaDialogAvailable", "")
407 .map(|v| v.value)
408 .map_err(Into::into)
409 })
410 }
411
412
413 fn inner_pick_files(
414 &self,
415 initial_location: Option<&FileUri>,
416 mime_types: &[&str],
417 multiple: bool,
418 ) -> crate::Result<Vec<FileUri>> {
419
420 on_android!({
421 impl_se!(struct Req<'a> {
422 mime_types: &'a [&'a str],
423 multiple: bool,
424 initial_location: Option<&'a FileUri>
425 });
426 impl_de!(struct Res { uris: Vec<FileUri> });
427
428 let _guard = self.0.intent_lock.lock();
429 self.0.api
430 .run_mobile_plugin::<Res>("showOpenFileDialog", Req { mime_types, multiple, initial_location })
431 .map(|v| v.uris)
432 .map_err(Into::into)
433 })
434 }
435
436 fn inner_pick_visual_medias(
437 &self,
438 target: VisualMediaTarget<'_>,
439 multiple: bool,
440 ) -> crate::Result<Vec<FileUri>> {
441
442 on_android!({
443 impl_se!(struct Req<'a> { multiple: bool, target: &'a str });
444 impl_de!(struct Res { uris: Vec<FileUri> });
445
446 let target = match target {
447 VisualMediaTarget::ImageOnly => "image/*",
448 VisualMediaTarget::VideoOnly => "video/*",
449 VisualMediaTarget::ImageAndVideo => "*/*",
450 VisualMediaTarget::ImageOrVideo { mime_type } => {
451 let is_image_or_video = mime_type.starts_with("image/") || mime_type.starts_with("video/");
452 if !is_image_or_video {
453 return Err(Error::with(format!("mime_type must be an image or a video, but {mime_type}")))
454 }
455
456 mime_type
457 }
458 };
459
460 let _guard = self.0.intent_lock.lock();
461 self.0.api
462 .run_mobile_plugin::<Res>("showOpenVisualMediaDialog", Req { multiple, target })
463 .map(|v| v.uris)
464 .map_err(Into::into)
465 })
466 }
467
468 fn inner_pick_contents(
469 &self,
470 mime_types: &[&str],
471 multiple: bool
472 ) -> crate::Result<Vec<FileUri>> {
473
474 on_android!({
475 impl_se!(struct Req<'a> { mime_types: &'a [&'a str], multiple: bool });
476 impl_de!(struct Res { uris: Vec<FileUri> });
477
478 let _guard = self.0.intent_lock.lock();
479 self.0.api
480 .run_mobile_plugin::<Res>("showOpenContentDialog", Req { mime_types, multiple })
481 .map(|v| v.uris)
482 .map_err(Into::into)
483 })
484 }
485}