tauri_plugin_android_fs/lib.rs
1//! Overview and usage is [here](https://crates.io/crates/tauri-plugin-android-fs)
2
3mod models;
4mod impls;
5mod error;
6
7use std::io::{Read, Write};
8
9pub use models::*;
10pub use error::{Error, Result};
11pub use impls::{AndroidFsExt, init};
12
13/// API
14pub trait AndroidFs<R: tauri::Runtime> {
15
16 /// Verify whether this plugin is available.
17 ///
18 /// On Android, this returns true.
19 /// On other platforms, this returns false.
20 fn is_available(&self) -> bool {
21 #[cfg(not(target_os = "android"))] {
22 false
23 }
24 #[cfg(target_os = "android")] {
25 true
26 }
27 }
28
29 /// Get the file or directory name.
30 ///
31 /// # Args
32 /// - **uri** :
33 /// Target uri.
34 /// This needs to be **readable**.
35 ///
36 /// # Support
37 /// All Android version.
38 fn get_name(&self, uri: &FileUri) -> crate::Result<String>;
39
40 /// Query the provider to get mime type.
41 /// If the directory, this returns `None`.
42 /// If the file, this returns no `None`.
43 /// If the file type is unknown or unset, this returns `Some("application/octet-stream")`.
44 ///
45 /// # Args
46 /// - **uri** :
47 /// Target uri.
48 /// This needs to be **readable**.
49 ///
50 /// # Support
51 /// All Android version.
52 fn get_mime_type(&self, uri: &FileUri) -> crate::Result<Option<String>>;
53
54 /// Queries the file system to get information about a file, directory.
55 ///
56 /// # Args
57 /// - **uri** :
58 /// Target uri.
59 /// This needs to be **readable**.
60 ///
61 /// # Note
62 /// This uses [`AndroidFs::open_file`] internally.
63 ///
64 /// # Support
65 /// All Android version.
66 fn get_metadata(&self, uri: &FileUri) -> crate::Result<std::fs::Metadata> {
67 let file = self.open_file(uri, FileAccessMode::Read)?;
68 Ok(file.metadata()?)
69 }
70
71 /// Open a file in the specified mode.
72 ///
73 /// # Note
74 /// This method uses a FileDescriptor internally.
75 /// However, if the target file does not physically exist on the device, such as cloud-based files,
76 /// the write operation using a FileDescriptor may not be reflected properly.
77 /// In such cases, consider using [AndroidFs::write_via_kotlin],
78 /// which writes using a standard method that does not rely on FileDescriptor,
79 /// or [AndroidFs::write], which automatically falls back to that approach when necessary.
80 /// If you specifically need to write using std::fs::File not entire contents, see [AndroidFs::write_via_kotlin_in] or [AndroidFs::copy_via_kotlin].
81 ///
82 /// It seems that the issue does not occur on all cloud storage platforms. At least, files on Google Drive have issues,
83 /// but files on Dropbox can be written to correctly using a FileDescriptor.
84 /// If you encounter issues with cloud storage other than Google Drive, please let me know on [Github](https://github.com/aiueo13/tauri-plugin-android-fs/issues/new).
85 /// This information will be used in [AndroidFs::need_write_via_kotlin] used by `AndroidFs::write`.
86 ///
87 /// There are no problems with file reading.
88 ///
89 /// # Support
90 /// All Android version.
91 fn open_file(&self, uri: &FileUri, mode: FileAccessMode) -> crate::Result<std::fs::File>;
92
93 /// Reads the entire contents of a file into a bytes vector.
94 ///
95 /// If you need to operate the file, use [`AndroidFs::open_file`] instead.
96 ///
97 /// # Args
98 /// - **uri** :
99 /// Target file uri.
100 /// This needs to be **readable**.
101 ///
102 /// # Support
103 /// All Android version.
104 fn read(&self, uri: &FileUri) -> crate::Result<Vec<u8>> {
105 let mut file = self.open_file(uri, FileAccessMode::Read)?;
106 let mut buf = file.metadata().ok()
107 .map(|m| m.len() as usize)
108 .map(Vec::with_capacity)
109 .unwrap_or_else(Vec::new);
110
111 file.read_to_end(&mut buf)?;
112 Ok(buf)
113 }
114
115 /// Reads the entire contents of a file into a string.
116 ///
117 /// If you need to operate the file, use [`AndroidFs::open_file`] instead.
118 ///
119 /// # Args
120 /// - **uri** :
121 /// Target file uri.
122 /// This needs to be **readable**.
123 ///
124 /// # Support
125 /// All Android version.
126 fn read_to_string(&self, uri: &FileUri) -> crate::Result<String> {
127 let mut file = self.open_file(uri, FileAccessMode::Read)?;
128 let mut buf = file.metadata().ok()
129 .map(|m| m.len() as usize)
130 .map(String::with_capacity)
131 .unwrap_or_else(String::new);
132
133 file.read_to_string(&mut buf)?;
134 Ok(buf)
135 }
136
137 /// Writes a slice as the entire contents of a file.
138 /// This function will entirely replace its contents if it does exist.
139 ///
140 /// If you want to operate the file, use [`AndroidFs::open_file`] instead.
141 ///
142 /// # Args
143 /// - **uri** :
144 /// Target file uri.
145 /// This needs to be **writable**.
146 ///
147 /// # Support
148 /// All Android version.
149 fn write(&self, uri: &FileUri, contents: impl AsRef<[u8]>) -> crate::Result<()> {
150 if self.need_write_via_kotlin(uri)? {
151 self.write_via_kotlin(uri, contents)?;
152 return Ok(())
153 }
154
155 let mut file = self.open_file(uri, FileAccessMode::WriteTruncate)?;
156 file.write_all(contents.as_ref())?;
157 Ok(())
158 }
159
160 /// Writes a slice as the entire contents of a file.
161 /// This function will entirely replace its contents if it does exist.
162 ///
163 /// Differences from `std::fs::File::write_all` is the process is done on Kotlin side.
164 /// See [`AndroidFs::open_file`] for why this function exists.
165 ///
166 /// If [`AndroidFs::write`] is used, it automatically fall back to this by [`AndroidFs::need_write_via_kotlin`],
167 /// so there should be few opportunities to use this.
168 ///
169 /// If you want to write using `std::fs::File`, not entire contents, use [`AndroidFs::write_via_kotlin_in`].
170 ///
171 /// # Inner process
172 /// The contents is written to a temporary file by Rust side
173 /// and then copied to the specified file on Kotlin side by [`AndroidFs::copy_via_kotlin`].
174 ///
175 /// # Support
176 /// All Android version.
177 fn write_via_kotlin(
178 &self,
179 uri: &FileUri,
180 contents: impl AsRef<[u8]>
181 ) -> crate::Result<()> {
182
183 self.write_via_kotlin_in(uri, |file| file.write_all(contents.as_ref()))
184 }
185
186 /// See [`AndroidFs::write_via_kotlin`] for information.
187 /// Use this if you want to write using `std::fs::File`, not entire contents.
188 ///
189 /// If you want to retain the file outside the closure,
190 /// you can perform the same operation using [`AndroidFs::copy_via_kotlin`] and [`PrivateStorage`].
191 /// For details, please refer to the internal implementation of this function.
192 ///
193 /// # Args
194 /// - **uri** :
195 /// Target file uri to write.
196 ///
197 /// - **contetns_writer** :
198 /// A closure that accepts a mutable reference to a `std::fs::File`
199 /// and performs the actual write operations. Note that this represents a temporary file.
200 fn write_via_kotlin_in<T>(
201 &self,
202 uri: &FileUri,
203 contents_writer: impl FnOnce(&mut std::fs::File) -> std::io::Result<T>
204 ) -> crate::Result<T> {
205
206 static TMP_FILE_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
207
208 // 一時ファイルの排他アクセスを保証
209 let _guard = TMP_FILE_LOCK.lock();
210
211 // 一時ファイルのパスを取得
212 let tmp_file_path = self.app_handle()
213 .android_fs()
214 .private_storage()
215 .resolve_path_with(PrivateDir::Cache, "tauri-plugin-android-fs-tempfile-write-via-kotlin")?;
216
217 // 一時ファイルに内容を書き込む
218 // エラー処理は一時ファイルを削除するまで保留
219 let result = {
220 let ref mut file = std::fs::File::create(&tmp_file_path)?;
221 contents_writer(file)
222 };
223
224 // 上記処理が成功していれば、一時ファイルの内容をkotlin側で指定されたファイルにコピーする
225 // エラー処理は一時ファイルを削除するまで保留
226 let result = result
227 .map_err(crate::Error::from)
228 .and_then(|t| self.copy_via_kotlin(&(&tmp_file_path).into(), uri).map(|_| t));
229
230 // 一時ファイルを削除
231 let _ = std::fs::remove_file(&tmp_file_path);
232
233 result
234 }
235
236 /// Determines if the file needs to be written via Kotlin side instead of Rust side.
237 /// Currently, this returns true only if the file is on GoogleDrive.
238 ///
239 /// # Support
240 /// All Android version.
241 fn need_write_via_kotlin(&self, uri: &FileUri) -> crate::Result<bool> {
242 Ok(uri.uri.starts_with("content://com.google.android.apps.docs.storage"))
243 }
244
245 /// Copies the contents of src file to dest.
246 ///
247 /// This copy process is done on Kotlin side, not on Rust.
248 /// Large files in GB units are also supported.
249 ///
250 /// See [`AndroidFs::write_via_kotlin`] for why this function exists.
251 ///
252 /// # Args
253 /// - **src** :
254 /// The uri of source file.
255 /// This needs to be **readable**.
256 ///
257 /// - **dest** :
258 /// The uri of destination file.
259 /// This needs to be **writable**.
260 ///
261 /// # Support
262 /// All Android version.
263 fn copy_via_kotlin(&self, src: &FileUri, dest: &FileUri) -> crate::Result<()>;
264
265 /// Remove the file.
266 ///
267 /// # Args
268 /// - **uri** :
269 /// Target file uri.
270 /// This needs to be **removable**.
271 ///
272 /// # Support
273 /// All Android version.
274 fn remove_file(&self, uri: &FileUri) -> crate::Result<()>;
275
276 /// Remove the **empty** directory.
277 ///
278 /// # Args
279 /// - **uri** :
280 /// Target directory uri.
281 /// This needs to be **removable**.
282 ///
283 /// # Support
284 /// All Android version.
285 fn remove_dir(&self, uri: &FileUri) -> crate::Result<()>;
286
287 /// Creates a new empty file in the specified location and returns a uri.
288 ///
289 /// The permissions and validity period of the returned uris depend on the origin directory
290 /// (e.g., the top directory selected by [`AndroidFs::show_open_dir_dialog`])
291 ///
292 /// # Args
293 /// - ***dir*** :
294 /// The uri of the base directory.
295 /// This needs to be **read-writable**.
296 ///
297 /// - ***relative_path*** :
298 /// The file path relative to the base directory.
299 /// If a file with the same name already exists, a sequential number will be appended to ensure uniqueness.
300 /// Any missing subdirectories in the specified path will be created automatically.
301 ///
302 /// - ***mime_type*** :
303 /// The MIME type of the file to be created.
304 /// Specifying this is recommended whenever possible.
305 /// If not provided, `application/octet-stream` will be used, as generic, unknown, or undefined file types.
306 ///
307 /// # Support
308 /// All Android version.
309 fn create_file(
310 &self,
311 dir: &FileUri,
312 relative_path: impl AsRef<str>,
313 mime_type: Option<&str>
314 ) -> crate::Result<FileUri>;
315
316 /// Returns the child files and directories of the specified directory.
317 /// The order of the entries is not guaranteed.
318 ///
319 /// The permissions and validity period of the returned uris depend on the origin directory
320 /// (e.g., the top directory selected by [`AndroidFs::show_open_dir_dialog`])
321 ///
322 /// # Args
323 /// - **uri** :
324 /// Target directory uri.
325 /// This needs to be **readable**.
326 ///
327 /// # Note
328 /// The returned type is an iterator because of the data formatting and the file system call is not executed lazily.
329 /// Thus, for directories with thousands or tens of thousands of elements, it may take several seconds.
330 ///
331 /// # Support
332 /// All Android version.
333 fn read_dir(&self, uri: &FileUri) -> crate::Result<impl Iterator<Item = Entry>>;
334
335 /// Opens a system file picker and returns a **read-writable** uris.
336 /// If no file is selected or the user cancels, an empty vec is returned.
337 ///
338 /// By default, returned uri is valid until the app is terminated.
339 /// If you want to persist it across app restarts, use [`AndroidFs::take_persistable_uri_permission`].
340 ///
341 /// Removing the returned files is also supported in most cases,
342 /// but note that files provided by third-party apps may not be removable.
343 ///
344 /// Just to read images and videos, consider using [`AndroidFs::show_open_visual_media_dialog`] instead.
345 ///
346 /// # Args
347 /// - ***initial_location*** :
348 /// Indicate the initial location of dialog.
349 /// System will do its best to launch the dialog in the specified entry
350 /// if it's a directory, or the directory that contains the specified file if not.
351 /// If this is missing or failed to resolve the desired initial location, the initial location is system specific.
352 ///
353 /// - ***mime_types*** :
354 /// The MIME types of the file to be selected.
355 /// However, there is no guarantee that the returned file will match the specified types.
356 /// If left empty, all file types will be available (equivalent to `["*/*"]`).
357 ///
358 /// - ***multiple*** :
359 /// Indicates whether multiple file selection is allowed.
360 ///
361 /// # Issue
362 /// This dialog has known issues. See the following for details and workarounds
363 /// - <https://github.com/aiueo13/tauri-plugin-android-fs/issues/1>
364 /// - <https://github.com/aiueo13/tauri-plugin-android-fs/blob/main/README.md>
365 ///
366 /// # Support
367 /// All Android version.
368 ///
369 /// # References
370 /// <https://developer.android.com/reference/android/content/Intent#ACTION_OPEN_DOCUMENT>
371 fn show_open_file_dialog(
372 &self,
373 initial_location: Option<&FileUri>,
374 mime_types: &[&str],
375 multiple: bool,
376 ) -> crate::Result<Vec<FileUri>>;
377
378 /// Opens a media picker and returns a **readonly** uris.
379 /// If no file is selected or the user cancels, an empty vec is returned.
380 ///
381 /// This media picker provides a browsable interface that presents the user with their media library,
382 /// sorted by date from newest to oldest.
383 ///
384 /// By default, returned uri is valid until the app is terminated.
385 /// If you want to persist it across app restarts, use [`AndroidFs::take_persistable_uri_permission`].
386 ///
387 /// # Args
388 /// - ***target*** :
389 /// The media type of the file to be selected.
390 /// Images or videos, or both.
391 ///
392 /// - ***multiple*** :
393 /// Indicates whether multiple file selection is allowed.
394 ///
395 /// # Issue
396 /// This dialog has known issues. See the following for details and workarounds
397 /// - <https://github.com/aiueo13/tauri-plugin-android-fs/issues/1>
398 /// - <https://github.com/aiueo13/tauri-plugin-android-fs/blob/main/README.md>
399 ///
400 /// # Note
401 /// The file obtained from this function cannot retrieve the correct file name using [`AndroidFs::get_name`].
402 /// Instead, it will be assigned a sequential number, such as `1000091523.png`.
403 /// And this is marked intended behavior, not a bug.
404 /// - <https://issuetracker.google.com/issues/268079113>
405 ///
406 /// # Support
407 /// This feature is available on devices that meet the following criteria:
408 /// - Running Android 11 (API level 30) or higher
409 /// - Receive changes to Modular System Components through Google System Updates
410 ///
411 /// Availability on a given device can be verified by calling [`AndroidFs::is_visual_media_dialog_available`].
412 /// If not supported, this function behaves the same as [`AndroidFs::show_open_file_dialog`].
413 ///
414 /// # References
415 /// <https://developer.android.com/training/data-storage/shared/photopicker>
416 fn show_open_visual_media_dialog(
417 &self,
418 target: VisualMediaTarget,
419 multiple: bool,
420 ) -> crate::Result<Vec<FileUri>>;
421
422 /// Opens a system directory picker, allowing the creation of a new directory or the selection of an existing one,
423 /// and returns a **read-write-removable** directory uri.
424 /// App can fully manage entries within the returned directory.
425 /// If no directory is selected or the user cancels, `None` is returned.
426 ///
427 /// By default, returned uri is valid until the app is terminated.
428 /// If you want to persist it across app restarts, use [`AndroidFs::take_persistable_uri_permission`].
429 ///
430 /// # Args
431 /// - ***initial_location*** :
432 /// Indicate the initial location of dialog.
433 /// System will do its best to launch the dialog in the specified entry
434 /// if it's a directory, or the directory that contains the specified file if not.
435 /// If this is missing or failed to resolve the desired initial location, the initial location is system specific.
436 ///
437 /// # Issue
438 /// This dialog has known issues. See the following for details and workarounds
439 /// - <https://github.com/aiueo13/tauri-plugin-android-fs/issues/1>
440 /// - <https://github.com/aiueo13/tauri-plugin-android-fs/blob/main/README.md>
441 ///
442 /// # Support
443 /// All Android version.
444 ///
445 /// # References
446 /// <https://developer.android.com/reference/android/content/Intent#ACTION_OPEN_DOCUMENT_TREE>
447 fn show_manage_dir_dialog(
448 &self,
449 initial_location: Option<&FileUri>,
450 ) -> crate::Result<Option<FileUri>>;
451
452 /// Please use [`AndroidFs::show_manage_dir_dialog`] instead.
453 #[deprecated = "Confusing name. Please use show_manage_dir_dialog instead."]
454 #[warn(deprecated)]
455 fn show_open_dir_dialog(&self) -> crate::Result<Option<FileUri>> {
456 self.show_manage_dir_dialog(None)
457 }
458
459 /// Opens a dialog to save a file and returns a **writeonly** uri.
460 /// The returned file may be a newly created file with no content,
461 /// or it may be an existing file with the requested MIME type.
462 /// If the user cancels, `None` is returned.
463 ///
464 /// By default, returned uri is valid until the app is terminated.
465 /// If you want to persist it across app restarts, use [`AndroidFs::take_persistable_uri_permission`].
466 ///
467 /// Removing and reading the returned files is also supported in most cases,
468 /// but note that files provided by third-party apps may not be removable.
469 ///
470 /// # Args
471 /// - ***initial_location*** :
472 /// Indicate the initial location of dialog.
473 /// System will do its best to launch the dialog in the specified entry
474 /// if it's a directory, or the directory that contains the specified file if not.
475 /// If this is missing or failed to resolve the desired initial location, the initial location is system specific.
476 ///
477 /// - ***initial_file_name*** :
478 /// An initial file name, but the user may change this value before creating the file.
479 ///
480 /// - ***mime_type*** :
481 /// The MIME type of the file to be saved.
482 /// Specifying this is recommended whenever possible.
483 /// If not provided, `application/octet-stream` will be used, as generic, unknown, or undefined file types.
484 ///
485 /// # Issue
486 /// This dialog has known issues. See the following for details and workarounds
487 /// - <https://github.com/aiueo13/tauri-plugin-android-fs/issues/1>
488 /// - <https://github.com/aiueo13/tauri-plugin-android-fs/blob/main/README.md>
489 ///
490 /// # Support
491 /// All Android version.
492 ///
493 /// # References
494 /// <https://developer.android.com/reference/android/content/Intent#ACTION_CREATE_DOCUMENT>
495 fn show_save_file_dialog(
496 &self,
497 initial_location: Option<&FileUri>,
498 initial_file_name: impl AsRef<str>,
499 mime_type: Option<&str>,
500 ) -> crate::Result<Option<FileUri>>;
501
502 /// Take persistent permission to access the file, directory and its descendants.
503 /// This is a prolongation of an already acquired permission, not the acquisition of a new one.
504 ///
505 /// This works by just calling, without displaying any confirmation to the user.
506 ///
507 /// 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)
508 /// Therefore, it is recommended to relinquish the unnecessary persisted uri by [`AndroidFs::release_persisted_uri_permission`] or [`AndroidFs::release_all_persisted_uri_permissions`].
509 /// Persisted permissions may be relinquished by other apps, user, or by moving/removing entries.
510 /// So check by [`AndroidFs::check_persisted_uri_permission`].
511 /// And you can retrieve the list of persisted uris using [`AndroidFs::get_all_persisted_uri_permissions`].
512 ///
513 /// # Args
514 /// - **uri** :
515 /// Uri of the target file or directory. This must be a uri taken from following :
516 /// - [`AndroidFs::show_open_file_dialog`]
517 /// - [`AndroidFs::show_open_visual_media_dialog`]
518 /// - [`AndroidFs::show_save_file_dialog`]
519 /// - [`AndroidFs::show_manage_dir_dialog`]
520 /// - [`AndroidFs::read_dir`] :
521 /// If this, the permissions of the origin directory uri is persisted, not a entry iteself.
522 /// Because the permissions and validity period of the entry uris depend on the origin directory.
523 ///
524 /// # Support
525 /// All Android version.
526 fn take_persistable_uri_permission(&self, uri: &FileUri) -> crate::Result<()>;
527
528 /// Check a persisted uri permission grant by [`AndroidFs::take_persistable_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 ///
535 /// - **mode** :
536 /// The mode of permission you want to check.
537 ///
538 /// # Support
539 /// All Android version.
540 fn check_persisted_uri_permission(&self, uri: &FileUri, mode: PersistableAccessMode) -> crate::Result<bool>;
541
542 /// Return list of all uri permission grants that have been persisted by [`AndroidFs::take_persistable_uri_permission`].
543 ///
544 /// # Support
545 /// All Android version.
546 fn get_all_persisted_uri_permissions(&self) -> crate::Result<impl Iterator<Item = PersistedUriPermission>>;
547
548 /// Relinquish a persisted uri permission grant by [`AndroidFs::take_persistable_uri_permission`].
549 ///
550 /// # Args
551 /// - **uri** :
552 /// Uri of the target file or directory.
553 ///
554 /// # Support
555 /// All Android version.
556 fn release_persisted_uri_permission(&self, uri: &FileUri) -> crate::Result<()>;
557
558 /// Relinquish a all persisted uri permission grants by [`AndroidFs::take_persistable_uri_permission`].
559 ///
560 /// # Support
561 /// All Android version.
562 fn release_all_persisted_uri_permissions(&self) -> crate::Result<()>;
563
564 /// Verify whether [`AndroidFs::show_open_visual_media_dialog`] is available on a given device.
565 ///
566 /// # Support
567 /// All Android version.
568 fn is_visual_media_dialog_available(&self) -> crate::Result<bool>;
569
570 /// File storage intended for the app’s use only.
571 fn private_storage(&self) -> &impl PrivateStorage<R>;
572
573 /// File storage that is available to other applications and users.
574 fn public_storage(&self) -> &impl PublicStorage<R>;
575
576 fn app_handle(&self) -> &tauri::AppHandle<R>;
577}
578
579/// File storage intended for the app’s use only.
580pub trait PublicStorage<R: tauri::Runtime> {
581
582 /// Creates a new empty file in the specified public app directory and returns a **persistent read-write-removable** URI.
583 ///
584 /// The created file will be registered with the corresponding MediaStore as needed.
585 ///
586 /// # Args
587 /// - ***dir*** :
588 /// The base directory.
589 ///
590 /// - ***relative_path*** :
591 /// The file path relative to the base directory.
592 /// If a file with the same name already exists, a sequential number will be appended to ensure uniqueness.
593 /// Any missing subdirectories in the specified path will be created automatically.
594 ///
595 /// - ***mime_type*** :
596 /// The MIME type of the file to be created.
597 /// Specifying this is recommended whenever possible.
598 /// If not provided, `application/octet-stream` will be used, as generic, unknown, or undefined file types.
599 /// When using [`PublicImageDir`], please use only image MIME types; using other types may cause errors.
600 /// Similarly, use only the corresponding media types for [`PublicVideoDir`] and [`PublicAudioDir`].
601 /// Only [`PublicGeneralPurposeDir`] supports all MIME types.
602 ///
603 /// # Support
604 /// Android 10 (API level 29) or higher.
605 /// Lower are need runtime request of `WRITE_EXTERNAL_STORAGE`. (This option will be made available in the future)
606 ///
607 /// [`PublicAudioDir::Audiobooks`] is not available on Android 9 (API level 28) and lower.
608 /// Availability on a given device can be verified by calling [`PublicStorage::is_audiobooks_dir_available`].
609 /// [`PublicAudioDir::Recordings`] is not available on Android 11 (API level 30) and lower.
610 /// Availability on a given device can be verified by calling [`PublicStorage::is_recordings_dir_available`].
611 /// Others are available in all Android versions.
612 fn create_file_in_public_app_dir(
613 &self,
614 dir: impl Into<PublicDir>,
615 relative_path: impl AsRef<str>,
616 mime_type: Option<&str>
617 ) -> crate::Result<FileUri> {
618
619 let config = self.app_handle().config();
620 let app_name = config.product_name.as_deref().unwrap_or("");
621 let app_name = match app_name.is_empty() {
622 true => &config.identifier,
623 false => app_name
624 };
625 let app_name = app_name.replace('/', " ");
626 let relative_path = relative_path.as_ref().trim_start_matches('/');
627 let relative_path_with_subdir = format!("{app_name}/{relative_path}");
628
629 self.create_file_in_public_dir(dir, relative_path_with_subdir, mime_type)
630 }
631
632 /// Creates a new empty file in the specified public directory and returns a **persistent read-write-removable** URI.
633 ///
634 /// The created file will be registered with the corresponding MediaStore as needed.
635 ///
636 /// # Args
637 /// - ***dir*** :
638 /// The base directory.
639 ///
640 /// - ***relative_path_with_subdir*** :
641 /// The file path relative to the base directory.
642 /// If a file with the same name already exists, a sequential number will be appended to ensure uniqueness.
643 /// Any missing subdirectories in the specified path will be created automatically.
644 /// Please specify a subdirectory in this,
645 /// such as `MyApp/file.txt` or `MyApp/2025-2-11/file.txt`. Do not use `file.txt`.
646 /// It is customary to specify the app name at the beginning of the subdirectory,
647 /// and in this case, using [`PublicStorage::create_file_in_public_app_dir`] is recommended.
648 ///
649 /// - ***mime_type*** :
650 /// The MIME type of the file to be created.
651 /// Specifying this is recommended whenever possible.
652 /// If not provided, `application/octet-stream` will be used, as generic, unknown, or undefined file types.
653 /// When using [`PublicImageDir`], please use only image MIME types; using other types may cause errors.
654 /// Similarly, use only the corresponding media types for [`PublicVideoDir`] and [`PublicAudioDir`].
655 /// Only [`PublicGeneralPurposeDir`] supports all MIME types.
656 ///
657 /// # Support
658 /// Android 10 (API level 29) or higher.
659 /// Lower are need runtime request of `WRITE_EXTERNAL_STORAGE`. (This option will be made available in the future)
660 ///
661 /// [`PublicAudioDir::Audiobooks`] is not available on Android 9 (API level 28) and lower.
662 /// Availability on a given device can be verified by calling [`PublicStorage::is_audiobooks_dir_available`].
663 /// [`PublicAudioDir::Recordings`] is not available on Android 11 (API level 30) and lower.
664 /// Availability on a given device can be verified by calling [`PublicStorage::is_recordings_dir_available`].
665 /// Others are available in all Android versions.
666 fn create_file_in_public_dir(
667 &self,
668 dir: impl Into<PublicDir>,
669 relative_path_with_subdir: impl AsRef<str>,
670 mime_type: Option<&str>
671 ) -> crate::Result<FileUri>;
672
673 /// Verify whether [`PublicAudioDir::Audiobooks`] is available on a given device.
674 ///
675 /// # Support
676 /// All Android version.
677 fn is_audiobooks_dir_available(&self) -> crate::Result<bool>;
678
679 /// Verify whether [`PublicAudioDir::Recordings`] is available on a given device.
680 ///
681 /// # Support
682 /// All Android version.
683 fn is_recordings_dir_available(&self) -> crate::Result<bool>;
684
685 fn app_handle(&self) -> &tauri::AppHandle<R>;
686}
687
688/// File storage intended for the app’s use only.
689pub trait PrivateStorage<R: tauri::Runtime> {
690
691 /// Get the absolute path of the specified directory.
692 /// App can fully manage entries within this directory without any permission via std::fs.
693 ///
694 /// These files will be deleted when the app is uninstalled and may also be deleted at the user’s initialising request.
695 /// When using [`PrivateDir::Cache`], the system will automatically delete files in this directory as disk space is needed elsewhere on the device.
696 ///
697 /// The returned path may change over time if the calling app is moved to an adopted storage device,
698 /// so only relative paths should be persisted.
699 ///
700 /// # Examples
701 /// ```no_run
702 /// use tauri_plugin_android_fs::{AndroidFs, AndroidFsExt, PrivateDir, PrivateStorage};
703 ///
704 /// fn example(app: tauri::AppHandle) {
705 /// let api = app.android_fs().private_storage();
706 ///
707 /// let dir_path = api.resolve_path(PrivateDir::Data).unwrap();
708 /// let file_path = dir_path.join("2025-2-12/data.txt");
709 ///
710 /// // Write file
711 /// std::fs::create_dir_all(file_path.parent().unwrap()).unwrap();
712 /// std::fs::write(&file_path, "aaaa").unwrap();
713 ///
714 /// // Read file
715 /// let _ = std::fs::read_to_string(&file_path).unwrap();
716 ///
717 /// // Remove file
718 /// std::fs::remove_file(&file_path).unwrap();
719 /// }
720 /// ```
721 ///
722 /// # Support
723 /// All Android version.
724 fn resolve_path(&self, dir: PrivateDir) -> crate::Result<std::path::PathBuf>;
725
726 /// Get the absolute path of the specified relative path and base directory.
727 /// App can fully manage entries of this path without any permission via std::fs.
728 ///
729 /// See [`PrivateStorage::resolve_path`] for details.
730 ///
731 /// # Support
732 /// All Android version.
733 fn resolve_path_with(
734 &self,
735 dir: PrivateDir,
736 relative_path: impl AsRef<str>
737 ) -> crate::Result<std::path::PathBuf> {
738
739 let relative_path = relative_path.as_ref().trim_start_matches('/');
740 let path = self.resolve_path(dir)?.join(relative_path);
741 Ok(path)
742 }
743
744 fn resolve_uri(&self, dir: PrivateDir) -> crate::Result<FileUri> {
745 self.resolve_path(dir).map(Into::into)
746 }
747
748 fn resolve_uri_with(&self, dir: PrivateDir, relative_path: impl AsRef<str>) -> crate::Result<FileUri> {
749 self.resolve_path_with(dir, relative_path).map(Into::into)
750 }
751
752 /// Writes a slice as the entire contents of a file.
753 ///
754 /// This function will create a file if it does not exist, and will entirely replace its contents if it does.
755 /// Recursively create parent directories if they are missing.
756 ///
757 /// This internally uses [`PrivateStorage::resolve_path`] , [`std::fs::create_dir_all`], and [`std::fs::write`].
758 /// See [`PrivateStorage::resolve_path`] for details.
759 ///
760 /// # Support
761 /// All Android version.
762 fn write(
763 &self,
764 base_dir: PrivateDir,
765 relative_path: impl AsRef<str>,
766 contents: impl AsRef<[u8]>
767 ) -> crate::Result<()> {
768
769 let path = self.resolve_path_with(base_dir, relative_path)?;
770
771 if let Some(parent_dir) = path.parent() {
772 std::fs::create_dir_all(parent_dir)?;
773 }
774
775 std::fs::write(path, contents)?;
776
777 Ok(())
778 }
779
780 /// Open a file in read-only mode.
781 ///
782 /// If you only need to read the entire file contents, consider using [`PrivateStorage::read`] or [`PrivateStorage::read_to_string`] instead.
783 ///
784 /// This internally uses [`PrivateStorage::resolve_path`] and [`std::fs::File::open`].
785 /// See [`PrivateStorage::resolve_path`] for details.
786 ///
787 /// # Support
788 /// All Android version.
789 fn open_file(
790 &self,
791 base_dir: PrivateDir,
792 relative_path: impl AsRef<str>,
793 ) -> crate::Result<std::fs::File> {
794
795 let path = self.resolve_path_with(base_dir, relative_path)?;
796 Ok(std::fs::File::open(path)?)
797 }
798
799 /// Opens a file in write-only mode.
800 /// This function will create a file if it does not exist, and will truncate it if it does.
801 ///
802 /// If you only need to write the contents, consider using [`PrivateStorage::write`] instead.
803 ///
804 /// This internally uses [`PrivateStorage::resolve_path`] and [`std::fs::File::create`].
805 /// See [`PrivateStorage::resolve_path`] for details.
806 ///
807 /// # Support
808 /// All Android version.
809 fn create_file(
810 &self,
811 base_dir: PrivateDir,
812 relative_path: impl AsRef<str>,
813 ) -> crate::Result<std::fs::File> {
814
815 let path = self.resolve_path_with(base_dir, relative_path)?;
816 Ok(std::fs::File::create(path)?)
817 }
818
819 /// Creates a new file in read-write mode; error if the file exists.
820 ///
821 /// This internally uses [`PrivateStorage::resolve_path`] and [`std::fs::File::create_new`].
822 /// See [`PrivateStorage::resolve_path`] for details.
823 ///
824 /// # Support
825 /// All Android version.
826 fn create_new_file(
827 &self,
828 base_dir: PrivateDir,
829 relative_path: impl AsRef<str>,
830 ) -> crate::Result<std::fs::File> {
831
832 let path = self.resolve_path_with(base_dir, relative_path)?;
833 Ok(std::fs::File::create_new(path)?)
834 }
835
836 /// Reads the entire contents of a file into a bytes vector.
837 ///
838 /// If you need [`std::fs::File`], use [`PrivateStorage::open_file`] insted.
839 ///
840 /// This internally uses [`PrivateStorage::resolve_path`] and [`std::fs::read`].
841 /// See [`PrivateStorage::resolve_path`] for details.
842 ///
843 /// # Support
844 /// All Android version.
845 fn read(
846 &self,
847 base_dir: PrivateDir,
848 relative_path: impl AsRef<str>,
849 ) -> crate::Result<Vec<u8>> {
850
851 let path = self.resolve_path_with(base_dir, relative_path)?;
852 Ok(std::fs::read(path)?)
853 }
854
855 /// Reads the entire contents of a file into a string.
856 ///
857 /// If you need [`std::fs::File`], use [`PrivateStorage::open_file`] insted.
858 ///
859 /// This internally uses [`PrivateStorage::resolve_path`] and [`std::fs::read_to_string`].
860 /// See [`PrivateStorage::resolve_path`] for details.
861 ///
862 /// # Support
863 /// All Android version.
864 fn read_to_string(
865 &self,
866 base_dir: PrivateDir,
867 relative_path: impl AsRef<str>,
868 ) -> crate::Result<String> {
869
870 let path = self.resolve_path_with(base_dir, relative_path)?;
871 Ok(std::fs::read_to_string(path)?)
872 }
873
874 /// Returns an iterator over the entries within a directory.
875 ///
876 /// This internally uses [`PrivateStorage::resolve_path`] and [`std::fs::read_dir`].
877 /// See [`PrivateStorage::resolve_path`] for details.
878 ///
879 /// # Support
880 /// All Android version.
881 fn read_dir(
882 &self,
883 base_dir: PrivateDir,
884 relative_path: Option<&str>,
885 ) -> crate::Result<std::fs::ReadDir> {
886
887 let path = match relative_path {
888 Some(relative_path) => self.resolve_path_with(base_dir, relative_path)?,
889 None => self.resolve_path(base_dir)?,
890 };
891
892 Ok(std::fs::read_dir(path)?)
893 }
894
895 /// Removes a file from the filesystem.
896 ///
897 /// This internally uses [`PrivateStorage::resolve_path`] and [`std::fs::remove_file`].
898 /// See [`PrivateStorage::resolve_path`] for details.
899 ///
900 /// # Support
901 /// All Android version.
902 fn remove_file(
903 &self,
904 base_dir: PrivateDir,
905 relative_path: impl AsRef<str>,
906 ) -> crate::Result<()> {
907
908 let path = self.resolve_path_with(base_dir, relative_path)?;
909 Ok(std::fs::remove_file(path)?)
910 }
911
912 /// Removes an empty directory.
913 /// If you want to remove a directory that is not empty, as well as all of its contents recursively, consider using [`PrivateStorage::remove_dir_all`] instead.
914 ///
915 /// This internally uses [`PrivateStorage::resolve_path`] and [`std::fs::remove_dir`].
916 /// See [`PrivateStorage::resolve_path`] for details.
917 ///
918 /// # Support
919 /// All Android version.
920 fn remove_dir(
921 &self,
922 base_dir: PrivateDir,
923 relative_path: Option<&str>,
924 ) -> crate::Result<()> {
925
926 let path = match relative_path {
927 Some(relative_path) => self.resolve_path_with(base_dir, relative_path)?,
928 None => self.resolve_path(base_dir)?,
929 };
930
931 std::fs::remove_dir(path)?;
932 Ok(())
933 }
934
935 /// Removes a directory at this path, after removing all its contents. Use carefully!
936 ///
937 /// This internally uses [`PrivateStorage::resolve_path`] and [`std::fs::remove_dir_all`].
938 /// See [`PrivateStorage::resolve_path`] for details.
939 ///
940 /// # Support
941 /// All Android version.
942 fn remove_dir_all(
943 &self,
944 base_dir: PrivateDir,
945 relative_path: Option<&str>,
946 ) -> crate::Result<()> {
947
948 let path = match relative_path {
949 Some(relative_path) => self.resolve_path_with(base_dir, relative_path)?,
950 None => self.resolve_path(base_dir)?,
951 };
952
953 std::fs::remove_dir_all(path)?;
954 Ok(())
955 }
956
957 /// Returns Ok(true) if the path points at an existing entity.
958 ///
959 /// This internally uses [`PrivateStorage::resolve_path`] and [`std::fs::exists`].
960 /// See [`PrivateStorage::resolve_path`] for details.
961 ///
962 /// # Support
963 /// All Android version.
964 fn exists(
965 &self,
966 base_dir: PrivateDir,
967 relative_path: impl AsRef<str>
968 ) -> crate::Result<bool> {
969
970 let path = self.resolve_path_with(base_dir, relative_path)?;
971 Ok(std::fs::exists(path)?)
972 }
973
974 /// Queries the file system to get information about a file, directory.
975 ///
976 /// This internally uses [`PrivateStorage::resolve_path`] and [`std::fs::metadata`].
977 /// See [`PrivateStorage::resolve_path`] for details.
978 ///
979 /// # Support
980 /// All Android version.
981 fn metadata(
982 &self,
983 base_dir: PrivateDir,
984 relative_path: Option<&str>,
985 ) -> crate::Result<std::fs::Metadata> {
986
987 let path = match relative_path {
988 Some(relative_path) => self.resolve_path_with(base_dir, relative_path)?,
989 None => self.resolve_path(base_dir)?,
990 };
991
992 Ok(std::fs::metadata(path)?)
993 }
994
995 fn app_handle(&self) -> &tauri::AppHandle<R>;
996}