tauri_plugin_android_fs/api/
public_storage.rs

1use crate::*;
2
3
4/// API of file storage that is available to other applications and users.  
5/// 
6/// # Examples
7/// ```
8/// fn example(app: &tauri::AppHandle) {
9///     use tauri_plugin_android_fs::AndroidFsExt as _;
10/// 
11///     let api = app.android_fs();
12///     let public_storage = api.public_storage();
13/// }
14/// ```
15pub struct PublicStorage<'a, R: tauri::Runtime>(
16    #[allow(unused)]
17    pub(crate) &'a AndroidFs<R>
18);
19
20impl<'a, R: tauri::Runtime> PublicStorage<'a, R> {
21
22    /// Gets a list of currently available storage volumes (internal storage, SD card, USB drive, etc.).
23    /// Be aware of TOCTOU.
24    /// 
25    /// This typically includes [`primary storage volume`](PublicStorage::get_primary_volume),
26    /// but it may occasionally be absent if the primary volume is inaccessible 
27    /// (e.g., mounted on a computer, removed, or another issue).
28    ///
29    /// Primary storage volume is always listed first, if included. 
30    /// But the order of the others is not guaranteed.  
31    ///
32    /// # Note
33    /// The volume represents the logical view of a storage volume for an individual user:
34    /// each user may have a different view for the same physical volume
35    /// (e.g. when the volume is a built-in emulated storage).
36    /// 
37    /// # Support
38    /// Android 10 (API level 29) or higher.  
39    pub fn get_available_volumes(&self) -> Result<Vec<PublicStorageVolume>> {
40        on_android!({
41            impl_de!(struct Obj { volume_name: String, description: Option<String>, is_primary: bool });
42            impl_de!(struct Res { volumes: Vec<Obj> });
43
44            let mut volumes = self.0.api
45                .run_mobile_plugin::<Res>("getStorageVolumes", "")
46                .map(|v| v.volumes.into_iter())
47                .map(|v| v.map(|v| 
48                    PublicStorageVolume {
49                        description: v.description.unwrap_or_else(|| v.volume_name.clone()),
50                        id: PublicStorageVolumeId(v.volume_name),
51                        is_primary: v.is_primary,
52                    }
53                ))
54                .map(|v| v.collect::<Vec<_>>())?;
55
56            // is_primary を先頭にする。他はそのままの順序
57            volumes.sort_by(|a, b| b.is_primary.cmp(&a.is_primary));
58
59            Ok(volumes)
60        })
61    }
62
63    /// Gets a primary storage volume.  
64    /// This is the most common and recommended storage volume for placing files that can be accessed by other apps or user.
65    /// 
66    /// A device always has one (and one only) primary storage volume.  
67    /// 
68    /// The returned primary volume may not currently be accessible 
69    /// if it has been mounted by the user on their computer, 
70    /// has been removed from the device, or some other problem has happened.  
71    /// 
72    /// You can find a list of all currently available volumes using [`PublicStorage::get_available_volumes`].
73    /// 
74    /// # Note
75    /// The volume represents the logical view of a storage volume for an individual user:
76    /// each user may have a different view for the same physical volume
77    /// (e.g. when the volume is a built-in emulated storage).
78    /// 
79    /// The primary volume provides a separate area for each user in a multi-user environment.
80    /// 
81    /// # Support
82    /// Android 10 (API level 29) or higher.   
83    /// 
84    /// # References
85    /// <https://developer.android.com/reference/android/provider/MediaStore#VOLUME_EXTERNAL_PRIMARY>
86    pub fn get_primary_volume(&self) -> Result<PublicStorageVolume> {
87        on_android!({
88            impl_de!(struct Res { volume_name: String, description: Option<String>, is_primary: bool });
89
90            self.0.api
91                .run_mobile_plugin::<Res>("getPrimaryStorageVolume", "")
92                .map(|v| 
93                    PublicStorageVolume {
94                        description: v.description.unwrap_or_else(|| v.volume_name.clone()),
95                        id: PublicStorageVolumeId(v.volume_name),
96                        is_primary: v.is_primary,
97                    }
98                )
99                .map_err(Into::into)
100        })
101    }
102
103    /// Creates a new empty file in the app folder of the specified public directory
104    /// and returns a **persistent read-write** URI.
105    ///
106    /// The created file has the following features:  
107    /// - It is registered with the appropriate MediaStore as needed.  
108    /// - The app can fully manage it until the app is uninstalled.  
109    /// - It is **not** removed when the app itself is uninstalled.  
110    ///
111    /// This is equivalent to:
112    /// ```ignore
113    /// let app_name = public_storage.app_dir_name()?;
114    /// public_storage.create_file(
115    ///     dir,
116    ///     format!("{app_name}/{relative_path}"),
117    ///     mime_type
118    /// )?;
119    /// ```
120    /// 
121    /// # Args
122    /// - ***volume*** :  
123    /// The storage volume, such as internal storage, SD card, etc.  
124    /// Usually, you don't need to specify this unless there is a special reason.  
125    /// If `None` is provided, [`the primary storage volume`](PublicStorage::get_primary_volume) will be used.  
126    /// 
127    /// - ***dir*** :  
128    /// The base directory.  
129    /// When using [`PublicImageDir`], use only image MIME types for ***mime_type***, which is discussed below.; using other types may cause errors.
130    /// Similarly, use only the corresponding media types for [`PublicVideoDir`] and [`PublicAudioDir`].
131    /// Only [`PublicGeneralPurposeDir`] supports all MIME types. 
132    ///  
133    /// - ***relative_path*** :  
134    /// The file path relative to the base directory.  
135    /// Any missing subdirectories in the specified path will be created automatically.  
136    /// If a file with the same name already exists, 
137    /// the system append a sequential number to ensure uniqueness.  
138    /// If no extension is present, 
139    /// the system may infer one from ***mime_type*** and may append it to the file name. 
140    /// But this append-extension operation depends on the model and version.
141    ///  
142    /// - ***mime_type*** :  
143    /// The MIME type of the file to be created.  
144    /// If this is None, MIME type is inferred from the extension of ***relative_path***
145    /// and if that fails, `application/octet-stream` is used.  
146    /// 
147    /// # Support
148    /// Android 10 (API level 29) or higher.  
149    ///
150    /// Note :  
151    /// - [`PublicAudioDir::Audiobooks`] is not available on Android 9 (API level 28) and lower.
152    /// Availability on a given device can be verified by calling [`PublicStorage::is_audiobooks_dir_available`].  
153    /// - [`PublicAudioDir::Recordings`] is not available on Android 11 (API level 30) and lower.
154    /// Availability on a given device can be verified by calling [`PublicStorage::is_recordings_dir_available`].  
155    /// - Others dirs are available in all Android versions.
156    pub fn create_file_in_app_dir(
157        &self,
158        volume: Option<&PublicStorageVolumeId>,
159        dir: impl Into<PublicDir>,
160        relative_path: impl AsRef<str>, 
161        mime_type: Option<&str>
162    ) -> crate::Result<FileUri> {
163
164        on_android!({
165            let app_dir_name = self.app_dir_name()?;
166            let relative_path = relative_path.as_ref().trim_matches('/');
167            let relative_path_with_subdir = format!("{app_dir_name}/{relative_path}");
168
169            self.create_file(volume, dir, relative_path_with_subdir, mime_type)
170        })
171    }
172
173    /// Creates a new empty file in the specified public directory
174    /// and returns a **persistent read-write** URI.
175    ///
176    /// The created file has the following features:  
177    /// - It is registered with the appropriate MediaStore as needed.  
178    /// - The app can fully manage it until the app is uninstalled.  
179    /// - It is **not** removed when the app itself is uninstalled.  
180    ///
181    /// # Args
182    /// - ***volume*** :  
183    /// The storage volume, such as internal storage, SD card, etc.  
184    /// Usually, you don't need to specify this unless there is a special reason.  
185    /// If `None` is provided, [`the primary storage volume`](PublicStorage::get_primary_volume) will be used.  
186    /// 
187    /// - ***dir*** :  
188    /// The base directory.  
189    /// When using [`PublicImageDir`], use only image MIME types for ***mime_type***, which is discussed below.; using other types may cause errors.
190    /// Similarly, use only the corresponding media types for [`PublicVideoDir`] and [`PublicAudioDir`].
191    /// Only [`PublicGeneralPurposeDir`] supports all MIME types. 
192    ///  
193    /// - ***relative_path_with_subdir*** :  
194    /// The file path relative to the base directory.  
195    /// Please specify a subdirectory in this, 
196    /// such as `MyApp/file.txt` or `MyApp/2025-2-11/file.txt`. Do not use `file.txt`.  
197    /// As shown above, it is customary to specify the app name at the beginning of the subdirectory, 
198    /// and in this case, using [`PublicStorage::create_file_in_app_dir`] is recommended.  
199    /// Any missing subdirectories in the specified path will be created automatically.  
200    /// If a file with the same name already exists, 
201    /// the system append a sequential number to ensure uniqueness.   
202    /// If no extension is present, 
203    /// the system may infer one from ***mime_type*** and may append it to the file name. 
204    /// But this append-extension operation depends on the model and version.
205    ///  
206    /// - ***mime_type*** :  
207    /// The MIME type of the file to be created.  
208    /// If this is None, MIME type is inferred from the extension of ***relative_path_with_subdir***
209    /// and if that fails, `application/octet-stream` is used.  
210    /// 
211    /// # Support
212    /// Android 10 (API level 29) or higher.  
213    ///
214    /// Note :  
215    /// - [`PublicAudioDir::Audiobooks`] is not available on Android 9 (API level 28) and lower.
216    /// Availability on a given device can be verified by calling [`PublicStorage::is_audiobooks_dir_available`].  
217    /// - [`PublicAudioDir::Recordings`] is not available on Android 11 (API level 30) and lower.
218    /// Availability on a given device can be verified by calling [`PublicStorage::is_recordings_dir_available`].  
219    /// - Others dirs are available in all Android versions.
220    pub fn create_file(
221        &self,
222        volume: Option<&PublicStorageVolumeId>,
223        dir: impl Into<PublicDir>,
224        relative_path_with_subdir: impl AsRef<str>, 
225        mime_type: Option<&str>
226    ) -> crate::Result<FileUri> {
227
228        on_android!({
229            impl_se!(struct Req<'a> { dir: PublicDir, dir_type: &'a str, volume_name: Option<&'a str> });
230            impl_de!(struct Res { name: String, uri: String });
231
232            if self.0.api_level()? < api_level::ANDROID_10 {
233                return Err(Error { msg: "requires Android 10 (API level 29) or higher".into() })
234            }
235
236            let volume_name = volume.map(|v| v.0.as_str());
237            let dir = dir.into();
238            let dir_type = match dir {
239                PublicDir::Image(_) => "Image",
240                PublicDir::Video(_) => "Video",
241                PublicDir::Audio(_) => "Audio",
242                PublicDir::GeneralPurpose(_) => "GeneralPurpose",
243            };
244
245            let (dir_name, dir_parent_uri) = self.0.api
246                .run_mobile_plugin::<Res>("getPublicDirInfo", Req { dir, dir_type, volume_name })
247                .map(|v| (v.name, v.uri))?;
248        
249            let relative_path = relative_path_with_subdir.as_ref().trim_matches('/');
250            let relative_path = format!("{dir_name}/{relative_path}");
251
252            let dir_parent_uri = FileUri {
253                uri: dir_parent_uri,
254                document_top_tree_uri: None
255            };
256
257            self.0.create_file(&dir_parent_uri, relative_path, mime_type)
258        })
259    }
260
261    /// Recursively create a directory and all of its parent components if they are missing.  
262    /// If it already exists, do nothing.
263    /// 
264    /// [`PublicStorage::create_file`] does this automatically, so there is no need to use it together.
265    /// 
266    /// # Args  
267    /// - ***volume*** :  
268    /// The storage volume, such as internal storage, SD card, etc.  
269    /// If `None` is provided, [`the primary storage volume`](PublicStorage::get_primary_volume) will be used.  
270    /// 
271    /// - ***dir*** :  
272    /// The base directory.  
273    ///  
274    /// - ***relative_path*** :  
275    /// The directory path relative to the base directory.    
276    ///  
277    /// # Support
278    /// Android 10 (API level 29) or higher.  
279    ///
280    /// Note :  
281    /// - [`PublicAudioDir::Audiobooks`] is not available on Android 9 (API level 28) and lower.
282    /// Availability on a given device can be verified by calling [`PublicStorage::is_audiobooks_dir_available`].  
283    /// - [`PublicAudioDir::Recordings`] is not available on Android 11 (API level 30) and lower.
284    /// Availability on a given device can be verified by calling [`PublicStorage::is_recordings_dir_available`].  
285    /// - Others dirs are available in all Android versions.
286    pub fn create_dir_all(
287        &self,
288        volume: Option<&PublicStorageVolumeId>,
289        dir: impl Into<PublicDir>,
290        relative_path: impl AsRef<str>, 
291    ) -> Result<()> {
292
293        on_android!({
294            let relative_path = relative_path.as_ref().trim_matches('/');
295            if relative_path.is_empty() {
296                return Ok(())
297            }
298
299            // TODO:
300            // create_file経由ではなく folder作成専用のkotlin apiを作成し呼び出す
301            let dir = dir.into();
302            let tmp_file_uri = self.create_file(
303                volume,
304                dir, 
305                format!("{relative_path}/TMP-01K3CGCKYSAQ1GHF8JW5FGD4RW"), 
306                Some(match dir {
307                    PublicDir::Image(_) => "image/png",
308                    PublicDir::Audio(_) => "audio/mp3",
309                    PublicDir::Video(_) => "video/mp4",
310                    PublicDir::GeneralPurpose(_) => "application/octet-stream"
311                })
312            )?;
313            let _ = self.0.remove_file(&tmp_file_uri);
314
315            Ok(())
316        })
317    }
318
319    /// Recursively create a directory and all of its parent components if they are missing.  
320    /// If it already exists, do nothing.
321    /// 
322    /// [`PublicStorage::create_file_in_app_dir`] does this automatically, so there is no need to use it together.  
323    /// 
324    /// This is the same as following: 
325    /// ```ignore
326    /// let app_name = public_storage.app_dir_name()?;
327    /// public_storage.create_dir_all(
328    ///     dir,
329    ///     format!("{app_name}/{relative_path}"),
330    /// )?;
331    /// ```
332    /// # Args  
333    /// - ***volume*** :  
334    /// The storage volume, such as internal storage, SD card, etc.  
335    /// If `None` is provided, [`the primary storage volume`](PublicStorage::get_primary_volume) will be used.  
336    /// 
337    /// - ***dir*** :  
338    /// The base directory.  
339    ///  
340    /// - ***relative_path*** :  
341    /// The directory path relative to the base directory.    
342    ///  
343    /// # Support
344    /// Android 10 (API level 29) or higher.  
345    ///
346    /// Note :  
347    /// - [`PublicAudioDir::Audiobooks`] is not available on Android 9 (API level 28) and lower.
348    /// Availability on a given device can be verified by calling [`PublicStorage::is_audiobooks_dir_available`].  
349    /// - [`PublicAudioDir::Recordings`] is not available on Android 11 (API level 30) and lower.
350    /// Availability on a given device can be verified by calling [`PublicStorage::is_recordings_dir_available`].  
351    /// - Others dirs are available in all Android versions.
352    pub fn create_dir_all_in_app_dir(
353        &self,
354        volume: Option<&PublicStorageVolumeId>,
355        dir: impl Into<PublicDir>,
356        relative_path: impl AsRef<str>, 
357    ) -> Result<()> {
358
359        on_android!({
360            let app_dir_name = self.app_dir_name()?;
361            let relative_path = relative_path.as_ref().trim_start_matches('/');
362            let relative_path_with_subdir = format!("{app_dir_name}/{relative_path}");
363
364            self.create_dir_all(volume, dir, relative_path_with_subdir)
365        })
366    }
367
368    /// Create the specified directory URI that has **no permissions**.  
369    /// 
370    /// This should only be used as `initial_location` in the file picker. 
371    /// It must not be used for any other purpose.  
372    /// 
373    /// This is useful when selecting save location, 
374    /// but when selecting existing entries, `initial_location` is often better with None.
375    /// 
376    /// # Example
377    /// ```rust
378    /// use tauri_plugin_android_fs::{AndroidFsExt, InitialLocation, PublicGeneralPurposeDir, PublicImageDir, PublicVideoDir};
379    ///
380    /// fn example(app: tauri::AppHandle) -> tauri_plugin_android_fs::Result<()> {
381    ///     let api = app.android_fs();
382    ///     let public_storage = api.public_storage();
383    ///
384    ///     // Get URI of the top public directory in primary volume
385    ///     let initial_location = public_storage.resolve_initial_location(
386    ///         InitialLocation::PrimaryTopDir, 
387    ///         false
388    ///     )?;
389    ///
390    ///     api.file_picker().pick_file(Some(&initial_location), &[])?;
391    ///     api.file_picker().pick_dir(Some(&initial_location))?;
392    ///     api.file_picker().save_file(Some(&initial_location), "file_name", Some("image/png"))?;
393    ///
394    ///
395    ///     // Get URI of ~/Pictures/ in primary volume
396    ///     let initial_location = public_storage.resolve_initial_location(
397    ///         InitialLocation::PrimaryPublicDir { 
398    ///             dir: PublicImageDir::Pictures.into(), 
399    ///             relative_path: "" 
400    ///         }, 
401    ///         false
402    ///     )?;
403    ///
404    ///     // Get URI of ~/Documents/sub_dir1/sub_dir2/ in primary volume
405    ///     let initial_location = public_storage.resolve_initial_location(
406    ///         InitialLocation::PrimaryPublicDir { 
407    ///             dir: PublicGeneralPurposeDir::Documents.into(), 
408    ///             relative_path: "sub_dir1/sub_dir2" 
409    ///         }, 
410    ///         true, // Create "sub_dir1" and "sub_dir2" directories if missing
411    ///     )?;
412    ///
413    ///
414    ///     let volumes = public_storage.get_available_volumes()?;
415    ///     let primary_volume = public_storage.get_primary_volume()?;
416    ///
417    ///     // Get any available volume other than the primary one 
418    ///     // (e.g., SD card or USB drive); 
419    ///     // 
420    ///     // if none, use the primary volume.
421    ///     let volume = volumes.into_iter()
422    ///         .find(|v| !v.is_primary)
423    ///         .unwrap_or(primary_volume);
424    ///
425    ///     // Get URI of the top public directory in the specified volume
426    ///     let initial_location = public_storage.resolve_initial_location(
427    ///         InitialLocation::TopDir {
428    ///             volume: &volume.id
429    ///         }, 
430    ///         false
431    ///     )?;
432    ///
433    ///     // Get URI of ~/Movies/ in the specified volume
434    ///     let initial_location = public_storage.resolve_initial_location(
435    ///         InitialLocation::PublicDir {
436    ///             volume: &volume.id,
437    ///             dir: PublicVideoDir::Movies.into(),
438    ///             relative_path: ""
439    ///         }, 
440    ///         false
441    ///     )?;
442    ///     
443    ///
444    ///     Ok(())
445    /// }
446    /// ```
447    /// 
448    /// # Support
449    /// All Android version.
450    ///
451    /// Note :  
452    /// - [`PublicAudioDir::Audiobooks`] is not available on Android 9 (API level 28) and lower.
453    /// Availability on a given device can be verified by calling [`PublicStorage::is_audiobooks_dir_available`].  
454    /// - [`PublicAudioDir::Recordings`] is not available on Android 11 (API level 30) and lower.
455    /// Availability on a given device can be verified by calling [`PublicStorage::is_recordings_dir_available`].  
456    /// - Others dirs are available in all Android versions.
457    pub fn resolve_initial_location(
458        &self,
459        initial_location: InitialLocation,
460        create_dir_all: bool
461    ) -> Result<FileUri> {
462
463        on_android!({
464            let volume = initial_location.volume();
465            let volume_uid = match volume {
466                None => None,
467                Some(volume) => self.get_volume_uuid(volume)?
468            };
469
470            let mut uri = match volume_uid {
471                None => "content://com.android.externalstorage.documents/document/primary%3A".to_string(),
472                Some(uid) => format!("content://com.android.externalstorage.documents/document/{uid}%3A")
473            };
474            
475            if let Some((dir, relative_path)) = initial_location.dir_and_relative_path(self.app_dir_name()?) {
476                uri.push_str(&format!("{dir}"));
477
478                let relative_path = relative_path.trim_matches('/');
479                if !relative_path.is_empty() {
480                    uri.push_str("%2F");
481                    uri.push_str(&encode_document_id(relative_path));
482                }
483
484                let _ = self.create_dir_all(volume, dir, relative_path);
485            }
486
487            Ok(FileUri { uri, document_top_tree_uri: None })
488        })
489    }
490
491    /// Verify whether the basic functions of PublicStorage 
492    /// (such as [`PublicStorage::create_file`]) can be performed.
493    /// 
494    /// If on Android 9 (API level 28) and lower, this returns false.  
495    /// If on Android 10 (API level 29) or higher, this returns true.  
496    /// 
497    /// # Support
498    /// All Android version.
499    pub fn is_available(&self) -> crate::Result<bool> {
500        on_android!({
501            Ok(api_level::ANDROID_10 <= self.0.api_level()?)
502        })
503    }
504
505    /// Verify whether [`PublicAudioDir::Audiobooks`] is available on a given device.   
506    /// 
507    /// If on Android 9 (API level 28) and lower, this returns false.  
508    /// If on Android 10 (API level 29) or higher, this returns true.  
509    /// 
510    /// # Support
511    /// All Android version.
512    pub fn is_audiobooks_dir_available(&self) -> crate::Result<bool> {
513        on_android!({
514            Ok(api_level::ANDROID_10 <= self.0.api_level()?)
515        })
516    }
517
518    /// Verify whether [`PublicAudioDir::Recordings`] is available on a given device.   
519    /// 
520    /// If on Android 11 (API level 30) and lower, this returns false.  
521    /// If on Android 12 (API level 31) or higher, this returns true.  
522    /// 
523    /// # Support
524    /// All Android version.
525    pub fn is_recordings_dir_available(&self) -> crate::Result<bool> {
526        on_android!({
527            Ok(api_level::ANDROID_12 <= self.0.api_level()?)
528        })
529    }
530
531    /// Resolve the app dir name from Tauri's config.  
532    /// 
533    /// # Support
534    /// All Android version.
535    pub fn app_dir_name(&self) -> crate::Result<&str> {
536        on_android!({
537            use std::sync::OnceLock;
538            
539            static APP_DIR_NAME: OnceLock<String> = OnceLock::new();
540
541            if APP_DIR_NAME.get().is_none() {
542                let config = self.0.app.config();
543                let app_name = config.product_name
544                    .as_deref()
545                    .filter(|s| !s.is_empty())
546                    .unwrap_or(&config.identifier)
547                    .replace('/', " ");
548                
549                // The cell is guaranteed to contain a value when set returns
550                let _ = APP_DIR_NAME.set(app_name);
551            }
552
553            Ok(&APP_DIR_NAME.get().unwrap())
554        })
555    }
556
557
558    #[allow(unused)]
559    fn get_volume_uuid(&self, volume: &PublicStorageVolumeId) -> Result<Option<String>> {
560        on_android!({
561            impl_se!(struct Req<'a> { volume_name: &'a str });
562            impl_de!(struct Res { value: Option<String> });
563
564            let volume_name = &volume.0;
565
566            self.0.api
567                .run_mobile_plugin::<Res>("getStorageVolumeUuid", Req { volume_name })
568                .map(|v| v.value)
569                .map_err(Into::into)
570        })
571    }
572}