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