tauri_plugin_android_fs/api/android_fs.rs
1#[allow(unused)]
2use std::io::{Read as _, Write as _};
3use crate::*;
4
5
6/// ***Root API***
7///
8/// # Examples
9/// ```
10/// fn example(app: &tauri::AppHandle) {
11/// use tauri_plugin_android_fs::AndroidFsExt as _;
12///
13/// let api = app.android_fs();
14/// }
15/// ```
16pub struct AndroidFs<R: tauri::Runtime> {
17 #[cfg(target_os = "android")]
18 pub(crate) app: tauri::AppHandle<R>,
19
20 #[cfg(target_os = "android")]
21 pub(crate) api: tauri::plugin::PluginHandle<R>,
22
23 #[cfg(target_os = "android")]
24 pub(crate) intent_lock: std::sync::Mutex<()>,
25
26 #[cfg(not(target_os = "android"))]
27 _marker: std::marker::PhantomData<fn() -> R>
28}
29
30impl<R: tauri::Runtime> AndroidFs<R> {
31
32 pub(crate) fn new<C: serde::de::DeserializeOwned>(
33 app: tauri::AppHandle<R>,
34 api: tauri::plugin::PluginApi<R, C>,
35 ) -> crate::Result<Self> {
36
37 #[cfg(target_os = "android")] {
38 Ok(Self {
39 api: api.register_android_plugin("com.plugin.android_fs", "AndroidFsPlugin")?,
40 app,
41 intent_lock: std::sync::Mutex::new(())
42 })
43 }
44
45 #[cfg(not(target_os = "android"))] {
46 Ok(Self { _marker: Default::default() })
47 }
48 }
49}
50
51impl<R: tauri::Runtime> AndroidFs<R> {
52
53 /// API of file storage intended for the app's use only.
54 pub fn private_storage(&self) -> PrivateStorage<'_, R> {
55 PrivateStorage(self)
56 }
57
58 /// API of file storage that is available to other applications and users.
59 pub fn public_storage(&self) -> PublicStorage<'_, R> {
60 PublicStorage(self)
61 }
62
63 /// API of file/dir picker.
64 pub fn file_picker(&self) -> FilePicker<'_, R> {
65 FilePicker(self)
66 }
67
68 /// API of sharing files with other apps.
69 pub fn file_sender(&self) -> FileSender<'_, R> {
70 FileSender(self)
71 }
72
73 /// Get the file or directory name.
74 ///
75 /// # Args
76 /// - ***uri*** :
77 /// Target URI.
78 /// Must be **readable**.
79 ///
80 /// # Support
81 /// All Android version.
82 pub fn get_name(&self, uri: &FileUri) -> crate::Result<String> {
83 on_android!({
84 impl_se!(struct Req<'a> { uri: &'a FileUri });
85 impl_de!(struct Res { name: String });
86
87 self.api
88 .run_mobile_plugin::<Res>("getName", Req { uri })
89 .map(|v| v.name)
90 .map_err(Into::into)
91 })
92 }
93
94 /// Queries the provider to get the MIME type.
95 ///
96 /// For files in [`PrivateStorage`], the MIME type is determined from the file extension.
97 /// In most other cases, it uses the MIME type that was associated with the file when it was created.
98 /// If the MIME type is unknown or unset, it falls back to `"application/octet-stream"`.
99 ///
100 /// If the target is a directory, an error will occur.
101 /// To check whether the target is a file or a directory, use [`AndroidFs::get_type`].
102 ///
103 /// # Args
104 /// - ***uri*** :
105 /// Target file URI.
106 /// Must be **readable**.
107 ///
108 /// # Support
109 /// All Android version.
110 pub fn get_mime_type(&self, uri: &FileUri) -> crate::Result<String> {
111 on_android!({
112 let mime_type = self.get_type(uri)?
113 .into_mime_type()
114 .ok_or_else(|| Error { msg: "This is not file".into() })?;
115
116 Ok(mime_type)
117 })
118 }
119
120 /// Gets the entry type.
121 ///
122 /// If the target is a directory, returns [`EntryType::Dir`].
123 ///
124 /// If the target is a file, returns [`EntryType::File { mime_type }`](EntryType::File).
125 /// For files in [`PrivateStorage`], the MIME type is determined from the file extension.
126 /// In most other cases, it uses the MIME type that was associated with the file when it was created.
127 /// If the MIME type is unknown or unset, it falls back to `"application/octet-stream"`.
128 ///
129 /// # Args
130 /// - ***uri*** :
131 /// Target URI.
132 /// Must be **readable**.
133 ///
134 /// # Support
135 /// All Android version.
136 pub fn get_type(&self, uri: &FileUri) -> crate::Result<EntryType> {
137 on_android!({
138 impl_se!(struct Req<'a> { uri: &'a FileUri });
139 impl_de!(struct Res { value: Option<String> });
140
141 self.api
142 .run_mobile_plugin::<Res>("getMimeType", Req { uri })
143 .map(|v| match v.value {
144 Some(mime_type) => EntryType::File { mime_type },
145 None => EntryType::Dir,
146 })
147 .map_err(Into::into)
148 })
149 }
150
151 /// Queries the file system to get information about a file, directory.
152 ///
153 /// # Args
154 /// - ***uri*** :
155 /// Target URI.
156 /// Must be **readable**.
157 ///
158 /// # Note
159 /// This uses [`AndroidFs::open_file`] internally.
160 ///
161 /// # Support
162 /// All Android version.
163 pub fn get_metadata(&self, uri: &FileUri) -> crate::Result<std::fs::Metadata> {
164 on_android!({
165 let file = self.open_file_readable(uri)?;
166 Ok(file.metadata()?)
167 })
168 }
169
170 /// Open the file in **readable** mode.
171 ///
172 /// # Note
173 /// If the target is a file on cloud storage or otherwise not physically present on the device,
174 /// the system creates a temporary file, downloads the data into it,
175 /// and then opens it. As a result, this processing may take longer than with regular local files.
176 ///
177 /// # Args
178 /// - ***uri*** :
179 /// Target file URI.
180 /// This need to be **readable**.
181 ///
182 /// # Support
183 /// All Android version.
184 pub fn open_file_readable(&self, uri: &FileUri) -> Result<std::fs::File> {
185 self.open_file(uri, FileAccessMode::Read)
186 }
187
188 /// Open the file in **writable** mode.
189 /// This truncates the existing contents.
190 ///
191 /// If you only need a [`std::io::Write`] instead of a [`std::fs::File`], or if applicable,
192 /// please use [`AndroidFs::open_writable_stream`] instead.
193 ///
194 /// # Note
195 /// If the target is a file on cloud storage or otherwise not actually present on the device,
196 /// writing with such files may not correctly reflect the changes.
197 /// If you need write with such files, use [`AndroidFs::open_writable_stream`].
198 ///
199 /// # Args
200 /// - ***uri*** :
201 /// Target file URI.
202 /// This need to be **writable**.
203 ///
204 /// # Support
205 /// All Android version.
206 pub fn open_file_writable(
207 &self,
208 uri: &FileUri,
209 ) -> crate::Result<std::fs::File> {
210
211 on_android!(#[allow(deprecated)] {
212 // Android 9 以下の場合、w は既存コンテンツを切り捨てる
213 if self.api_level()? <= api_level::ANDROID_9 {
214 self.open_file(uri, FileAccessMode::Write)
215 }
216 // Android 10 以上の場合、w は既存コンテンツの切り捨てを保証しない。
217 // そのため切り捨ててファイルを開くには wt を用いる必要があるが、
218 // wt は全ての file provider が対応しているとは限らないため、
219 // フォールバックを用いてなるべく多くの状況に対応する。
220 // https://issuetracker.google.com/issues/180526528?pli=1
221 else {
222 let (file, mode) = self.open_file_with_fallback(uri, [
223 FileAccessMode::WriteTruncate,
224 FileAccessMode::ReadWriteTruncate,
225 FileAccessMode::Write
226 ])?;
227
228 if mode == FileAccessMode::Write {
229 // file provider が既存コンテンツを切り捨てず、
230 // かつ書き込むデータ量が元のそれより少ない場合、ファイルが壊れる可能性がある。
231 // これを避けるため強制的にデータを切り捨てる。
232 // file provider の実装によっては set_len は失敗することがある。
233 file.set_len(0)?;
234 }
235
236 Ok(file)
237 }
238 })
239 }
240
241 /// Open the file in the specified mode.
242 ///
243 /// # Note
244 /// If the target is a file on cloud storage or otherwise not physically present on the device,
245 /// the system creates a temporary file, downloads the data into it,
246 /// and then opens it. As a result, this processing may take longer than with regular local files.
247 ///
248 /// When writing to a file with this function,
249 /// pay attention to the following points:
250 ///
251 /// 1. **Files that do not exist on the device**:
252 /// Writing to files that are not physically present on the device may not correctly
253 /// reflect changes. If you need to write to such files, use [`AndroidFs::open_writable_stream`].
254 ///
255 /// 2. **File mode restrictions**:
256 /// Files provided by third-party apps may not support writable modes other than
257 /// [`FileAccessMode::Write`]. However, [`FileAccessMode::Write`] does not guarantee
258 /// that existing contents will always be truncated.
259 /// As a result, if the new contents are shorter than the original, the file may
260 /// become corrupted. To avoid this, consider using
261 /// [`AndroidFs::open_file_writable`] or [`AndroidFs::open_writable_stream`], which
262 /// ensure that existing contents are truncated and also automatically apply the
263 /// maximum possible fallbacks.
264 /// <https://issuetracker.google.com/issues/180526528>
265 ///
266 /// # Args
267 /// - ***uri*** :
268 /// Target file URI.
269 /// This must have corresponding permissions (read, write, or both) for the specified ***mode***.
270 ///
271 /// - ***mode*** :
272 /// Indicates how the file is opened and the permissions granted.
273 /// Note that files hosted by third-party apps may not support following:
274 /// - [`FileAccessMode::ReadWrite`]
275 /// - [`FileAccessMode::ReadWriteTruncate`]
276 /// - [`FileAccessMode::WriteAppend`]
277 /// - [`FileAccessMode::WriteTruncate`]
278 ///
279 /// # Support
280 /// All Android version.
281 pub fn open_file(&self, uri: &FileUri, mode: FileAccessMode) -> crate::Result<std::fs::File> {
282 on_android!({
283 impl_se!(struct Req<'a> { uri: &'a FileUri, mode: &'a str });
284 impl_de!(struct Res { fd: std::os::fd::RawFd });
285
286 let mode = mode.to_mode();
287
288 self.api
289 .run_mobile_plugin::<Res>("getFileDescriptor", Req { uri, mode })
290 .map(|v| {
291 use std::os::fd::FromRawFd;
292 unsafe { std::fs::File::from_raw_fd(v.fd) }
293 })
294 .map_err(Into::into)
295 })
296 }
297
298 /// For detailed documentation and notes, see [`AndroidFs::open_file`].
299 ///
300 /// The modes specified in ***candidate_modes*** are tried in order.
301 /// If the file can be opened, this returns the file along with the mode used.
302 /// If all attempts fail, an error is returned.
303 pub fn open_file_with_fallback(
304 &self,
305 uri: &FileUri,
306 candidate_modes: impl IntoIterator<Item = FileAccessMode>
307 ) -> crate::Result<(std::fs::File, FileAccessMode)> {
308
309 on_android!({
310 impl_se!(struct Req<'a> { uri: &'a FileUri, modes: Vec<&'a str> });
311 impl_de!(struct Res { fd: std::os::fd::RawFd, mode: String });
312
313 let modes = candidate_modes.into_iter().map(|m| m.to_mode()).collect::<Vec<_>>();
314
315 if modes.is_empty() {
316 return Err(Error::with("candidate_modes must not be empty"));
317 }
318
319 self.api
320 .run_mobile_plugin::<Res>("getFileDescriptorWithFallback", Req { uri, modes })
321 .map_err(Into::into)
322 .and_then(|v| FileAccessMode::from_mode(&v.mode).map(|m| (v.fd, m)))
323 .map(|(fd, mode)| {
324 let file = {
325 use std::os::fd::FromRawFd;
326 unsafe { std::fs::File::from_raw_fd(fd) }
327 };
328 (file, mode)
329 })
330 })
331 }
332
333 /// Opens a stream for writing to the specified file.
334 /// This truncates the existing contents.
335 ///
336 /// # Usage
337 /// [`WritableStream`] implements [`std::io::Write`], so it can be used for writing.
338 /// As with [`std::fs::File`], wrap it with [`std::io::BufWriter`] if buffering is needed.
339 ///
340 /// After writing, call [`WritableStream::reflect`].
341 ///
342 /// # Note
343 /// The behavior depends on [`AndroidFs::need_write_via_kotlin`].
344 /// If it is `false`, this behaves like [`AndroidFs::open_file_writable`].
345 /// If it is `true`, this behaves like [`AndroidFs::open_writable_stream_via_kotlin`].
346 ///
347 /// # Args
348 /// - ***uri*** :
349 /// Target file URI.
350 /// This need to be **writable**.
351 ///
352 /// # Support
353 /// All Android version.
354 pub fn open_writable_stream(
355 &self,
356 uri: &FileUri
357 ) -> Result<WritableStream<R>> {
358
359 on_android!({
360 let need_write_via_kotlin = self.need_write_via_kotlin(uri)?;
361 WritableStream::new(self.app.clone(), uri.clone(), need_write_via_kotlin)
362 })
363 }
364
365 /// Opens a writable stream to the specified file.
366 /// This truncates the existing contents.
367 ///
368 /// This function always writes content via the Kotlin API,
369 /// ensuring that writes to files not physically present on the device, such as cloud storage files, are properly applied.
370 /// But this takes several times longer compared to [`AndroidFs::open_writable_stream`].
371 ///
372 /// [`AndroidFs::open_writable_stream`] automatically falls back to this function depending on [`AndroidFs::need_write_via_kotlin`].
373 /// For performance reasons, it is strongly recommended to use [`AndroidFs::open_writable_stream`]
374 /// if you can tolerate that [`AndroidFs::need_write_via_kotlin`] may not cover all cases.
375 ///
376 /// # Usage
377 /// [`WritableStream`] implements [`std::io::Write`], so it can be used for writing.
378 /// As with [`std::fs::File`], wrap it with [`std::io::BufWriter`] if buffering is needed.
379 ///
380 /// After writing, call [`WritableStream::reflect`].
381 ///
382 /// # Args
383 /// - ***uri*** :
384 /// Target file URI.
385 /// This need to be **writable**.
386 ///
387 /// # Support
388 /// All Android version.
389 pub fn open_writable_stream_via_kotlin(
390 &self,
391 uri: &FileUri
392 ) -> Result<WritableStream<R>> {
393
394 on_android!({
395 let need_write_via_kotlin = true;
396 WritableStream::new(self.app.clone(), uri.clone(), need_write_via_kotlin)
397 })
398 }
399
400 /// Reads the entire contents of a file into a bytes vector.
401 ///
402 /// # Args
403 /// - ***uri*** :
404 /// Target file URI.
405 /// Must be **readable**.
406 ///
407 /// # Support
408 /// All Android version.
409 pub fn read(&self, uri: &FileUri) -> crate::Result<Vec<u8>> {
410 on_android!({
411 let mut file = self.open_file_readable(uri)?;
412 let mut buf = file.metadata().ok()
413 .map(|m| m.len() as usize)
414 .map(Vec::with_capacity)
415 .unwrap_or_else(Vec::new);
416
417 file.read_to_end(&mut buf)?;
418 Ok(buf)
419 })
420 }
421
422 /// Reads the entire contents of a file into a string.
423 ///
424 /// # Args
425 /// - ***uri*** :
426 /// Target file URI.
427 /// Must be **readable**.
428 ///
429 /// # Support
430 /// All Android version.
431 pub fn read_to_string(&self, uri: &FileUri) -> crate::Result<String> {
432 on_android!({
433 let mut file = self.open_file_readable(uri)?;
434 let mut buf = file.metadata().ok()
435 .map(|m| m.len() as usize)
436 .map(String::with_capacity)
437 .unwrap_or_else(String::new);
438
439 file.read_to_string(&mut buf)?;
440 Ok(buf)
441 })
442 }
443
444 /// Writes a slice as the entire contents of a file.
445 /// This function will entirely replace its contents if it does exist.
446 ///
447 /// # Note
448 /// The behavior depends on [`AndroidFs::need_write_via_kotlin`].
449 /// If it is `false`, this uses [`std::fs::File::write_all`].
450 /// If it is `true`, this uses [`AndroidFs::write_via_kotlin`].
451 ///
452 /// # Args
453 /// - ***uri*** :
454 /// Target file URI.
455 /// Must be **writable**.
456 ///
457 /// # Support
458 /// All Android version.
459 pub fn write(&self, uri: &FileUri, contents: impl AsRef<[u8]>) -> crate::Result<()> {
460 on_android!({
461 let mut stream = self.open_writable_stream(uri)?;
462 stream.write_all(contents.as_ref())?;
463 stream.reflect()?;
464 Ok(())
465 })
466 }
467
468 /// Writes a slice as the entire contents of a file.
469 /// This function will entirely replace its contents if it does exist.
470 ///
471 /// This function always writes content via the Kotlin API,
472 /// ensuring that writes to files not physically present on the device, such as cloud storage files, are properly applied.
473 /// But this takes several times longer compared to [`AndroidFs::write`].
474 ///
475 /// [`AndroidFs::write`] automatically falls back to this function depending on [`AndroidFs::need_write_via_kotlin`].
476 /// For performance reasons, it is strongly recommended to use [`AndroidFs::write`]
477 /// if you can tolerate that [`AndroidFs::need_write_via_kotlin`] may not cover all cases.
478 ///
479 /// # Support
480 /// All Android version.
481 pub fn write_via_kotlin(
482 &self,
483 uri: &FileUri,
484 contents: impl AsRef<[u8]>
485 ) -> crate::Result<()> {
486
487 on_android!({
488 let mut stream = self.open_writable_stream_via_kotlin(uri)?;
489 stream.write_all(contents.as_ref())?;
490 stream.reflect()?;
491 Ok(())
492 })
493 }
494
495 /// Copies the contents of the source file to the destination.
496 /// If the destination already has contents, they are truncated before writing the source contents.
497 ///
498 /// # Note
499 /// The behavior depends on [`AndroidFs::need_write_via_kotlin`].
500 /// If it is `false`, this uses [`std::io::copy`] with [`std::fs::File`].
501 /// If it is `true`, this uses [`AndroidFs::copy_via_kotlin`].
502 ///
503 /// # Args
504 /// - ***src*** :
505 /// The URI of source file.
506 /// Must be **readable**.
507 ///
508 /// - ***dest*** :
509 /// The URI of destination file.
510 /// Must be **writable**.
511 ///
512 /// # Support
513 /// All Android version.
514 pub fn copy(&self, src: &FileUri, dest: &FileUri) -> crate::Result<()> {
515 on_android!({
516 // NOTE:
517 // std::io::copy は std::fs::File 同士のコピーの場合、最適化が働く可能性がある。
518 // そのため self.open_writable_stream は用いない。
519
520 if self.need_write_via_kotlin(dest)? {
521 self.copy_via_kotlin(src, dest, None)?;
522 }
523 else {
524 let src = &mut self.open_file_readable(src)?;
525 let dest = &mut self.open_file_writable(dest)?;
526 std::io::copy(src, dest)?;
527 }
528 Ok(())
529 })
530 }
531
532 /// Copies the contents of src file to dest.
533 /// If dest already has contents, it is truncated before write src contents.
534 ///
535 /// This function always writes content via the Kotlin API,
536 /// ensuring that writes to files not physically present on the device, such as cloud storage files, are properly applied.
537 ///
538 /// [`AndroidFs::copy`] automatically falls back to this function depending on [`AndroidFs::need_write_via_kotlin`].
539 ///
540 /// # Args
541 /// - ***src*** :
542 /// The URI of source file.
543 /// Must be **readable**.
544 ///
545 /// - ***dest*** :
546 /// The URI of destination file.
547 /// Must be **writable**.
548 ///
549 /// - ***buffer_size***:
550 /// The size of the buffer, in bytes, to use during the copy process on Kotlin.
551 /// If `None`, [`DEFAULT_BUFFER_SIZE`](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.io/-d-e-f-a-u-l-t_-b-u-f-f-e-r_-s-i-z-e.html) is used.
552 /// At least, when I checked, it was 8 KB.
553 /// If zero, this causes error.
554 ///
555 /// # Support
556 /// All Android version.
557 pub fn copy_via_kotlin(
558 &self,
559 src: &FileUri,
560 dest: &FileUri,
561 buffer_size: Option<u32>,
562 ) -> crate::Result<()> {
563
564 on_android!({
565 impl_se!(struct Req<'a> { src: &'a FileUri, dest: &'a FileUri, buffer_size: Option<u32> });
566 impl_de!(struct Res;);
567
568 if buffer_size.is_some_and(|s| s <= 0) {
569 return Err(Error { msg: "buffer_size must be non zero".into() })
570 }
571
572 self.api
573 .run_mobile_plugin::<Res>("copyFile", Req { src, dest, buffer_size })
574 .map(|_| ())
575 .map_err(Into::into)
576 })
577 }
578
579 /// Determines whether the file must be written via the Kotlin API rather than through a file descriptor.
580 ///
581 /// **This may not cover all cases.**
582 ///
583 /// # Why
584 /// For files that do not physically exist on the device, such as those stored in cloud storage,
585 /// writing data through [`std::fs::File`] may not be properly reflected.
586 /// In such cases, the write operation must be performed via the Kotlin API.
587 /// <https://community.latenode.com/t/csv-export-to-google-drive-results-in-empty-file-but-local-storage-works-fine>
588 ///
589 /// # Support
590 /// All Android version.
591 pub fn need_write_via_kotlin(&self, uri: &FileUri) -> crate::Result<bool> {
592 on_android!({
593 // Note:
594 // Android 15 の Dropbox (ver. 440.2.4) では fd 経由で書き込んで反映された
595
596 const URI_PREFIXES: &'static [&'static str] = &[
597 "content://com.google.android.apps.docs", // Google drive
598 ];
599
600 Ok(URI_PREFIXES.iter().any(|prefix| uri.uri.starts_with(prefix)))
601 })
602 }
603
604 /// Renames a file or directory to a new name, and return new URI.
605 /// Even if the names conflict, the existing file will not be overwritten.
606 ///
607 /// Note that when files or folders (and their descendants) are renamed, their URIs will change, and any previously granted permissions will be lost.
608 /// In other words, this function returns a new URI without any permissions.
609 /// However, for files created in PublicStorage, the URI remains unchanged even after such operations, and all permissions are retained.
610 /// In this, this function returns the same URI as original URI.
611 ///
612 /// # Args
613 /// - ***uri*** :
614 /// URI of target entry.
615 ///
616 /// - ***new_name*** :
617 /// New name of target entry.
618 /// This include extension if use.
619 /// The behaviour in the same name already exists depends on the file provider.
620 /// In the case of e.g. [`PublicStorage`], the suffix (e.g. `(1)`) is added to this name.
621 /// In the case of files hosted by other applications, errors may occur.
622 /// But at least, the existing file will not be overwritten.
623 /// The system may sanitize these strings as needed, so those strings may not be used as it is.
624 ///
625 /// # Support
626 /// All Android version.
627 pub fn rename(&self, uri: &FileUri, new_name: impl AsRef<str>) -> crate::Result<FileUri> {
628 on_android!({
629 impl_se!(struct Req<'a> { uri: &'a FileUri, new_name: &'a str });
630
631 let new_name = new_name.as_ref();
632
633 self.api
634 .run_mobile_plugin::<FileUri>("rename", Req { uri, new_name })
635 .map_err(Into::into)
636 })
637 }
638
639 /// Remove the file.
640 ///
641 /// # Args
642 /// - ***uri*** :
643 /// Target file URI.
644 /// Must be **writable**, at least. But even if it is,
645 /// removing may not be possible in some cases.
646 /// For details, refer to the documentation of the function that provided the URI.
647 /// If not file, an error will occur.
648 ///
649 /// # Support
650 /// All Android version.
651 pub fn remove_file(&self, uri: &FileUri) -> crate::Result<()> {
652 on_android!({
653 impl_se!(struct Req<'a> { uri: &'a FileUri });
654 impl_de!(struct Res;);
655
656 self.api
657 .run_mobile_plugin::<Res>("deleteFile", Req { uri })
658 .map(|_| ())
659 .map_err(Into::into)
660 })
661 }
662
663 /// Remove the **empty** directory.
664 ///
665 /// # Args
666 /// - ***uri*** :
667 /// Target directory URI.
668 /// Must be **writable**.
669 /// If not empty directory, an error will occur.
670 ///
671 /// # Support
672 /// All Android version.
673 pub fn remove_dir(&self, uri: &FileUri) -> crate::Result<()> {
674 on_android!({
675 impl_se!(struct Req<'a> { uri: &'a FileUri });
676 impl_de!(struct Res;);
677
678 self.api
679 .run_mobile_plugin::<Res>("deleteEmptyDir", Req { uri })
680 .map(|_| ())
681 .map_err(Into::into)
682 })
683 }
684
685 /// Removes a directory and all its contents. Use carefully!
686 ///
687 /// # Args
688 /// - ***uri*** :
689 /// Target directory URI.
690 /// Must be **writable**.
691 /// If not directory, an error will occur.
692 ///
693 /// # Support
694 /// All Android version.
695 pub fn remove_dir_all(&self, uri: &FileUri) -> crate::Result<()> {
696 on_android!({
697 impl_se!(struct Req<'a> { uri: &'a FileUri });
698 impl_de!(struct Res;);
699
700 self.api
701 .run_mobile_plugin::<Res>("deleteDirAll", Req { uri })
702 .map(|_| ())
703 .map_err(Into::into)
704 })
705 }
706
707 /// Build a URI of an **existing** file located at the relative path from the specified directory.
708 /// Error occurs, if the file does not exist.
709 ///
710 /// The permissions and validity period of the returned URI depend on the origin directory
711 /// (e.g., the top directory selected by [`FilePicker::pick_dir`])
712 ///
713 /// # Note
714 /// For [`AndroidFs::create_new_file`] and etc, the system may sanitize path strings as needed, so those strings may not be used as it is.
715 /// However, this function does not perform any sanitization, so the same ***relative_path*** may still fail.
716 ///
717 /// # Support
718 /// All Android version.
719 pub fn try_resolve_file_uri(
720 &self,
721 dir: &FileUri,
722 relative_path: impl AsRef<std::path::Path>
723 ) -> crate::Result<FileUri> {
724
725 on_android!({
726 #[allow(deprecated)]
727 let uri = self.resolve_uri(dir, relative_path)?;
728
729 if !self.get_type(&uri)?.is_file() {
730 return Err(crate::Error::with(format!("This is not a file: {uri:?}")))
731 }
732 Ok(uri)
733 })
734 }
735
736 /// Build a URI of an **existing** directory located at the relative path from the specified directory.
737 /// Error occurs, if the directory does not exist.
738 ///
739 /// The permissions and validity period of the returned URI depend on the origin directory
740 /// (e.g., the top directory selected by [`FilePicker::pick_dir`])
741 ///
742 /// # Note
743 /// For [`AndroidFs::create_new_file`] and etc, the system may sanitize path strings as needed, so those strings may not be used as it is.
744 /// However, this function does not perform any sanitization, so the same ***relative_path*** may still fail.
745 ///
746 /// # Support
747 /// All Android version.
748 pub fn try_resolve_dir_uri(
749 &self,
750 dir: &FileUri,
751 relative_path: impl AsRef<std::path::Path>
752 ) -> crate::Result<FileUri> {
753
754 on_android!({
755 #[allow(deprecated)]
756 let uri = self.resolve_uri(dir, relative_path)?;
757
758 if !self.get_type(&uri)?.is_dir() {
759 return Err(crate::Error::with(format!("This is not a directory: {uri:?}")))
760 }
761 Ok(uri)
762 })
763 }
764
765 /// Build a URI of an entry located at the relative path from the specified directory.
766 ///
767 /// This function does **not** create any entries; it only constructs the URI.
768 ///
769 /// This function does not perform checks on the arguments or the returned URI.
770 /// Even if the dir argument refers to a file, no error occurs (and no panic either).
771 /// Instead, it simply returns an invalid URI that will cause errors if used with other functions.
772 ///
773 /// If you need check, consider using [`AndroidFs::try_resolve_file_uri`] or [`AndroidFs::try_resolve_dir_uri`] instead.
774 /// Or use this with [`AndroidFs::get_mime_type`].
775 ///
776 /// The permissions and validity period of the returned URI depend on the origin directory
777 /// (e.g., the top directory selected by [`FilePicker::pick_dir`])
778 ///
779 /// # Note
780 /// For [`PublicStorage::create_new_file`] and etc, the system may sanitize path strings as needed, so those strings may not be used as it is.
781 /// However, this function does not perform any sanitization, so the same ***relative_path*** may still fail.
782 ///
783 /// # Performance
784 /// This operation is relatively fast
785 /// because it does not call Kotlin API and only involves operating strings on Rust side.
786 ///
787 /// # Support
788 /// All Android version.
789 #[deprecated = "Use AndroidFs::try_resolve_file_uri or AndroidFs::try_resolve_dir_uri instead"]
790 pub fn resolve_uri(
791 &self,
792 dir: &FileUri,
793 relative_path: impl AsRef<std::path::Path>
794 ) -> crate::Result<FileUri> {
795
796 on_android!({
797 let base_dir = &dir.uri;
798 let relative_path = validate_relative_path(relative_path.as_ref())?;
799 let relative_path = relative_path.to_string_lossy();
800
801 if relative_path.is_empty() {
802 return Ok(dir.clone())
803 }
804
805 Ok(FileUri {
806 document_top_tree_uri: dir.document_top_tree_uri.clone(),
807 uri: format!("{base_dir}%2F{}", encode_document_id(relative_path))
808 })
809 })
810 }
811
812 /// See [`AndroidFs::get_thumbnail_to`] for descriptions.
813 ///
814 /// If thumbnail does not wrote to dest, return false.
815 pub fn get_thumbnail_to(
816 &self,
817 src: &FileUri,
818 dest: &FileUri,
819 preferred_size: Size,
820 format: ImageFormat,
821 ) -> crate::Result<bool> {
822
823 on_android!({
824 impl_se!(struct Req<'a> {
825 src: &'a FileUri,
826 dest: &'a FileUri,
827 format: &'a str,
828 quality: u8,
829 width: u32,
830 height: u32,
831 });
832 impl_de!(struct Res { value: bool });
833
834 let (quality, format) = match format {
835 ImageFormat::Png => (1.0, "Png"),
836 ImageFormat::Jpeg => (0.75, "Jpeg"),
837 ImageFormat::Webp => (0.7, "Webp"),
838 ImageFormat::JpegWith { quality } => (quality, "Jpeg"),
839 ImageFormat::WebpWith { quality } => (quality, "Webp"),
840 };
841 let quality = (quality * 100.0).clamp(0.0, 100.0) as u8;
842 let Size { width, height } = preferred_size;
843
844 self.api
845 .run_mobile_plugin::<Res>("getThumbnail", Req { src, dest, format, quality, width, height })
846 .map(|v| v.value)
847 .map_err(Into::into)
848 })
849 }
850
851 /// Query the provider to get a file thumbnail.
852 /// If thumbnail does not exist it, return None.
853 ///
854 /// Note this does not cache. Please do it in your part if need.
855 ///
856 /// # Args
857 /// - ***uri*** :
858 /// Targe file uri.
859 /// Thumbnail availablty depends on the file provider.
860 /// In general, images and videos are available.
861 /// For files in [`PrivateStorage`],
862 /// the file type must match the filename extension.
863 ///
864 /// - ***preferred_size*** :
865 /// Optimal thumbnail size desired.
866 /// This may return a thumbnail of a different size,
867 /// but never more than double the requested size.
868 /// In any case, the aspect ratio is maintained.
869 ///
870 /// - ***format*** :
871 /// Thumbnail image format.
872 /// [`ImageFormat::Jpeg`] is recommended.
873 /// If you need transparency, use others.
874 ///
875 /// # Support
876 /// All Android version.
877 pub fn get_thumbnail(
878 &self,
879 uri: &FileUri,
880 preferred_size: Size,
881 format: ImageFormat,
882 ) -> crate::Result<Option<Vec<u8>>> {
883
884 on_android!({
885 let (tmp_file, tmp_file_path) = self.private_storage().create_new_tmp_file()?;
886 std::mem::drop(tmp_file);
887
888 let result = self.get_thumbnail_to(uri, &(&tmp_file_path).into(), preferred_size, format)
889 .and_then(|ok| {
890 if ok {
891 std::fs::read(&tmp_file_path)
892 .map(Some)
893 .map_err(Into::into)
894 }
895 else {
896 Ok(None)
897 }
898 });
899
900 let _ = std::fs::remove_file(&tmp_file_path);
901
902 result
903 })
904 }
905
906 /// Creates a new empty file in the specified location and returns a URI.
907 ///
908 /// The permissions and validity period of the returned URIs depend on the origin directory
909 /// (e.g., the top directory selected by [`FilePicker::pick_dir`])
910 ///
911 /// # Args
912 /// - ***dir*** :
913 /// The URI of the base directory.
914 /// Must be **read-write**.
915 ///
916 /// - ***relative_path*** :
917 /// The file path relative to the base directory.
918 /// Any missing subdirectories in the specified path will be created automatically.
919 /// If a file with the same name already exists,
920 /// the system append a sequential number to ensure uniqueness.
921 /// If no extension is present,
922 /// the system may infer one from ***mime_type*** and may append it to the file name.
923 /// But this append-extension operation depends on the model and version.
924 /// The system may sanitize these strings as needed, so those strings may not be used as it is.
925 ///
926 /// - ***mime_type*** :
927 /// The MIME type of the file to be created.
928 /// If this is None, MIME type is inferred from the extension of ***relative_path***
929 /// and if that fails, `application/octet-stream` is used.
930 ///
931 /// # Support
932 /// All Android version.
933 pub fn create_new_file(
934 &self,
935 dir: &FileUri,
936 relative_path: impl AsRef<std::path::Path>,
937 mime_type: Option<&str>
938 ) -> crate::Result<FileUri> {
939
940 on_android!({
941 impl_se!(struct Req<'a> { dir: &'a FileUri, mime_type: Option<&'a str>, relative_path: &'a str });
942
943 let relative_path = validate_relative_path(relative_path.as_ref())?;
944 let relative_path = relative_path.to_string_lossy();
945
946 self.api
947 .run_mobile_plugin::<FileUri>("createFile", Req { dir, mime_type, relative_path: relative_path.as_ref() })
948 .map_err(Into::into)
949 })
950 }
951
952 /// Recursively create a directory and all of its parent components if they are missing,
953 /// then return the URI.
954 /// If it already exists, do nothing and just return the direcotry uri.
955 ///
956 /// [`AndroidFs::create_new_file`] does this automatically, so there is no need to use it together.
957 ///
958 /// # Args
959 /// - ***dir*** :
960 /// The URI of the base directory.
961 /// Must be **read-write**.
962 ///
963 /// - ***relative_path*** :
964 /// The directory path relative to the base directory.
965 /// The system may sanitize these strings as needed, so those strings may not be used as it is.
966 ///
967 /// # Support
968 /// All Android version.
969 pub fn create_dir_all(
970 &self,
971 dir: &FileUri,
972 relative_path: impl AsRef<std::path::Path>,
973 ) -> Result<FileUri> {
974
975 on_android!({
976 impl_se!(struct Req<'a> { dir: &'a FileUri,relative_path: &'a str });
977
978 let relative_path = validate_relative_path(relative_path.as_ref())?;
979 let relative_path = relative_path.to_string_lossy();
980
981 self.api
982 .run_mobile_plugin::<FileUri>("createDirAll", Req { dir, relative_path: relative_path.as_ref() })
983 .map_err(Into::into)
984 })
985 }
986
987 /// Returns the child files and directories of the specified directory.
988 /// The order of the entries is not guaranteed.
989 ///
990 /// The permissions and validity period of the returned URIs depend on the origin directory
991 /// (e.g., the top directory selected by [`FilePicker::pick_dir`])
992 ///
993 /// # Note
994 /// The returned type is an iterator, but the file system call is not executed lazily.
995 /// Instead, all data is retrieved at once.
996 /// For directories containing thousands or even tens of thousands of entries,
997 /// this function may take several seconds to complete.
998 /// The returned iterator itself is low-cost, as it only performs lightweight data formatting.
999 ///
1000 /// # Args
1001 /// - ***uri*** :
1002 /// Target directory URI.
1003 /// Must be **readable**.
1004 ///
1005 /// # Support
1006 /// All Android version.
1007 pub fn read_dir(&self, uri: &FileUri) -> crate::Result<impl Iterator<Item = Entry>> {
1008 on_android!(std::iter::Empty::<_>, {
1009 impl_se!(struct Req<'a> { uri: &'a FileUri });
1010 impl_de!(struct Obj { name: String, uri: FileUri, last_modified: i64, byte_size: i64, mime_type: Option<String> });
1011 impl_de!(struct Res { entries: Vec<Obj> });
1012
1013 use std::time::{UNIX_EPOCH, Duration};
1014
1015 self.api
1016 .run_mobile_plugin::<Res>("readDir", Req { uri })
1017 .map(|v| v.entries.into_iter())
1018 .map(|v| v.map(|v| match v.mime_type {
1019 Some(mime_type) => Entry::File {
1020 name: v.name,
1021 last_modified: UNIX_EPOCH + Duration::from_millis(v.last_modified as u64),
1022 len: v.byte_size as u64,
1023 mime_type,
1024 uri: v.uri,
1025 },
1026 None => Entry::Dir {
1027 name: v.name,
1028 last_modified: UNIX_EPOCH + Duration::from_millis(v.last_modified as u64),
1029 uri: v.uri,
1030 }
1031 }))
1032 .map_err(Into::into)
1033 })
1034 }
1035
1036 /// Take persistent permission to access the file, directory and its descendants.
1037 /// This is a prolongation of an already acquired permission, not the acquisition of a new one.
1038 ///
1039 /// This works by just calling, without displaying any confirmation to the user.
1040 ///
1041 /// 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)
1042 /// Therefore, it is recommended to relinquish the unnecessary persisted URI by [`AndroidFs::release_persisted_uri_permission`] or [`AndroidFs::release_all_persisted_uri_permissions`].
1043 /// Persisted permissions may be relinquished by other apps, user, or by moving/removing entries.
1044 /// So check by [`AndroidFs::check_persisted_uri_permission`].
1045 /// And you can retrieve the list of persisted uris using [`AndroidFs::get_all_persisted_uri_permissions`].
1046 ///
1047 /// # Args
1048 /// - **uri** :
1049 /// URI of the target file or directory.
1050 /// This must be a URI taken from following :
1051 /// - [`FilePicker::pick_files`]
1052 /// - [`FilePicker::pick_file`]
1053 /// - [`FilePicker::pick_visual_medias`]
1054 /// - [`FilePicker::pick_visual_media`]
1055 /// - [`FilePicker::pick_dir`]
1056 /// - [`FilePicker::save_file`]
1057 /// - [`AndroidFs::try_resolve_file_uri`], [`AndroidFs::try_resolve_dir_uri`], [`AndroidFs::resolve_uri`], [`AndroidFs::read_dir`], [`AndroidFs::create_new_file`], [`AndroidFs::create_dir_all`] :
1058 /// If use URI from thoese fucntions, the permissions of the origin directory URI is persisted, not a entry iteself by this function.
1059 /// Because the permissions and validity period of the descendant entry URIs depend on the origin directory.
1060 ///
1061 /// # Support
1062 /// All Android version.
1063 pub fn take_persistable_uri_permission(&self, uri: &FileUri) -> crate::Result<()> {
1064 on_android!({
1065 impl_se!(struct Req<'a> { uri: &'a FileUri });
1066 impl_de!(struct Res;);
1067
1068 self.api
1069 .run_mobile_plugin::<Res>("takePersistableUriPermission", Req { uri })
1070 .map(|_| ())
1071 .map_err(Into::into)
1072 })
1073 }
1074
1075 /// Check a persisted URI permission grant by [`AndroidFs::take_persistable_uri_permission`].
1076 /// Returns false if there are only non-persistent permissions or no permissions.
1077 ///
1078 /// # Args
1079 /// - **uri** :
1080 /// URI of the target file or directory.
1081 /// This must be a URI taken from following :
1082 /// - [`FilePicker::pick_files`]
1083 /// - [`FilePicker::pick_file`]
1084 /// - [`FilePicker::pick_visual_medias`]
1085 /// - [`FilePicker::pick_visual_media`]
1086 /// - [`FilePicker::pick_dir`]
1087 /// - [`FilePicker::save_file`]
1088 /// - [`AndroidFs::try_resolve_file_uri`], [`AndroidFs::try_resolve_dir_uri`], [`AndroidFs::resolve_uri`], [`AndroidFs::read_dir`], [`AndroidFs::create_new_file`], [`AndroidFs::create_dir_all`] :
1089 /// If use URI from thoese fucntions, the permissions of the origin directory URI is checked, not a entry iteself by this function.
1090 /// Because the permissions and validity period of the descendant entry URIs depend on the origin directory.
1091 ///
1092 /// - **mode** :
1093 /// The mode of permission you want to check.
1094 ///
1095 /// # Support
1096 /// All Android version.
1097 pub fn check_persisted_uri_permission(&self, uri: &FileUri, mode: PersistableAccessMode) -> crate::Result<bool> {
1098 on_android!({
1099 impl_se!(struct Req<'a> { uri: &'a FileUri, mode: PersistableAccessMode });
1100 impl_de!(struct Res { value: bool });
1101
1102 self.api
1103 .run_mobile_plugin::<Res>("checkPersistedUriPermission", Req { uri, mode })
1104 .map(|v| v.value)
1105 .map_err(Into::into)
1106 })
1107 }
1108
1109 /// Return list of all persisted URIs that have been persisted by [`AndroidFs::take_persistable_uri_permission`] and currently valid.
1110 ///
1111 /// # Support
1112 /// All Android version.
1113 pub fn get_all_persisted_uri_permissions(&self) -> crate::Result<impl Iterator<Item = PersistedUriPermission>> {
1114 on_android!(std::iter::Empty::<_>, {
1115 impl_de!(struct Obj { uri: FileUri, r: bool, w: bool, d: bool });
1116 impl_de!(struct Res { items: Vec<Obj> });
1117
1118 self.api
1119 .run_mobile_plugin::<Res>("getAllPersistedUriPermissions", "")
1120 .map(|v| v.items.into_iter())
1121 .map(|v| v.map(|v| {
1122 let (uri, can_read, can_write) = (v.uri, v.r, v.w);
1123 match v.d {
1124 true => PersistedUriPermission::Dir { uri, can_read, can_write },
1125 false => PersistedUriPermission::File { uri, can_read, can_write }
1126 }
1127 }))
1128 .map_err(Into::into)
1129 })
1130 }
1131
1132 /// Relinquish a persisted URI permission grant by [`AndroidFs::take_persistable_uri_permission`].
1133 ///
1134 /// # Args
1135 /// - ***uri*** :
1136 /// URI of the target file or directory.
1137 ///
1138 /// # Support
1139 /// All Android version.
1140 pub fn release_persisted_uri_permission(&self, uri: &FileUri) -> crate::Result<()> {
1141 on_android!({
1142 impl_se!(struct Req<'a> { uri: &'a FileUri });
1143 impl_de!(struct Res;);
1144
1145 self.api
1146 .run_mobile_plugin::<Res>("releasePersistedUriPermission", Req { uri })
1147 .map(|_| ())
1148 .map_err(Into::into)
1149 })
1150 }
1151
1152 /// Relinquish a all persisted uri permission grants by [`AndroidFs::take_persistable_uri_permission`].
1153 ///
1154 /// # Support
1155 /// All Android version.
1156 pub fn release_all_persisted_uri_permissions(&self) -> crate::Result<()> {
1157 on_android!({
1158 impl_de!(struct Res);
1159
1160 self.api
1161 .run_mobile_plugin::<Res>("releaseAllPersistedUriPermissions", "")
1162 .map(|_| ())
1163 .map_err(Into::into)
1164 })
1165 }
1166
1167 /// Verify whether this plugin is available.
1168 ///
1169 /// On Android, this returns true.
1170 /// On other platforms, this returns false.
1171 pub fn is_available(&self) -> bool {
1172 cfg!(target_os = "android")
1173 }
1174
1175 /// Get the api level of this Android device.
1176 ///
1177 /// The correspondence table between API levels and Android versions can be found following.
1178 /// <https://developer.android.com/guide/topics/manifest/uses-sdk-element#api-level-table>
1179 ///
1180 /// If you want the constant value of the API level from an Android version, there is the [`api_level`] module.
1181 ///
1182 /// # Table
1183 /// | Android version | API Level |
1184 /// |------------------|-----------|
1185 /// | 16.0 | 36 |
1186 /// | 15.0 | 35 |
1187 /// | 14.0 | 34 |
1188 /// | 13.0 | 33 |
1189 /// | 12L | 32 |
1190 /// | 12.0 | 31 |
1191 /// | 11.0 | 30 |
1192 /// | 10.0 | 29 |
1193 /// | 9.0 | 28 |
1194 /// | 8.1 | 27 |
1195 /// | 8.0 | 26 |
1196 /// | 7.1 - 7.1.2 | 25 |
1197 /// | 7.0 | 24 |
1198 ///
1199 /// Tauri does not support Android versions below 7.
1200 pub fn api_level(&self) -> Result<i32> {
1201 on_android!({
1202 impl_de!(struct Res { value: i32 });
1203
1204 static API_LEVEL: std::sync::OnceLock<i32> = std::sync::OnceLock::new();
1205
1206 if let Some(api_level) = API_LEVEL.get() {
1207 return Ok(*api_level)
1208 }
1209
1210 let api_level = self.api
1211 .run_mobile_plugin::<Res>("getApiLevel", "")?
1212 .value;
1213
1214 let _ = API_LEVEL.set(api_level);
1215
1216 Ok(api_level)
1217 })
1218 }
1219}