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    /// Since read-only SD cards and similar cases may be included, 
26    /// please use [`StorageVolume { is_readonly, .. }`](StorageVolume) for filtering as needed.
27    /// 
28    /// This typically includes [`primary storage volume`](PublicStorage::get_primary_volume),
29    /// but it may occasionally be absent if the primary volume is inaccessible 
30    /// (e.g., mounted on a computer, removed, or another issue).
31    ///
32    /// Primary storage volume is always listed first, if included. 
33    /// But the order of the others is not guaranteed.  
34    ///
35    /// # Note
36    /// The volume represents the logical view of a storage volume for an individual user:
37    /// each user may have a different view for the same physical volume.
38    /// In other words, it provides a separate area for each user in a multi-user environment.
39    /// 
40    /// # Support
41    /// Android 10 (API level 29) or higher.  
42    pub fn get_volumes(&self) -> Result<Vec<StorageVolume>> {
43        if self.0.api_level()? < api_level::ANDROID_10 {
44            return Err(Error { msg: "requires Android 10 (API level 29) or higher".into() })
45        }
46
47        let volumes = self.0.get_available_storage_volumes()?
48            .into_iter()
49            .filter(|v| v.id.media_store_volume_name.is_some())
50            .collect::<Vec<_>>();
51
52        Ok(volumes)
53    }
54
55    /// Gets a primary storage volume.  
56    /// This is the most common and recommended storage volume for placing files that can be accessed by other apps or user.
57    /// In many cases, it is device's built-in storage.  
58    /// 
59    /// A device always has one (and one only) primary storage volume.  
60    /// 
61    /// Primary volume may not currently be accessible 
62    /// if it has been mounted by the user on their computer, 
63    /// has been removed from the device, or some other problem has happened. 
64    /// If so, this returns `None`.
65    /// 
66    /// # Note
67    /// The volume represents the logical view of a storage volume for an individual user:
68    /// each user may have a different view for the same physical volume.
69    /// In other words, it provides a separate area for each user in a multi-user environment.
70    /// 
71    /// # Support
72    /// Android 10 (API level 29) or higher.   
73    pub fn get_primary_volume(&self) -> Result<Option<StorageVolume>> {
74        if self.0.api_level()? < api_level::ANDROID_10 {
75            return Err(Error { msg: "requires Android 10 (API level 29) or higher".into() })
76        }
77
78        self.0.get_primary_storage_volume_if_available()
79            .map(|v| v.filter(|v| v.id.media_store_volume_name.is_some()))
80            .map_err(Into::into)
81    }
82
83    /// Creates a new empty file in the specified public directory of the storage volume.  
84    /// This returns a **persistent read-write** URI.
85    ///
86    /// The created file has the following features:  
87    /// - It is registered with the appropriate MediaStore as needed.  
88    /// - The app can fully manage it until the app is uninstalled.  
89    /// - It is **not** removed when the app itself is uninstalled.  
90    /// 
91    /// # Args
92    /// - ***volume_id*** :  
93    /// ID of the storage volume, such as internal storage, SD card, etc.  
94    /// Usually, you don't need to specify this unless there is a special reason.  
95    /// If `None` is provided, [`the primary storage volume`](PublicStorage::get_primary_volume) will be used.  
96    /// 
97    /// - ***base_dir*** :  
98    /// The base directory.  
99    /// When using [`PublicImageDir`], use only image MIME types for ***mime_type***, which is discussed below.; using other types may cause errors.
100    /// Similarly, use only the corresponding media types for [`PublicVideoDir`] and [`PublicAudioDir`].
101    /// Only [`PublicGeneralPurposeDir`] supports all MIME types. 
102    /// 
103    /// - ***relative_path*** :  
104    /// The file path relative to the base directory.  
105    /// To avoid cluttering files, it is helpful to place the app name directory at the top level.   
106    /// Any missing subdirectories in the specified path will be created automatically.  
107    /// If a file with the same name already exists, 
108    /// the system append a sequential number to ensure uniqueness.  
109    /// If no extension is present, 
110    /// the system may infer one from ***mime_type*** and may append it to the file name. 
111    /// But this append-extension operation depends on the model and version.  
112    /// The system may sanitize these strings as needed, so those strings may not be used as it is.
113    ///  
114    /// - ***mime_type*** :  
115    /// The MIME type of the file to be created.  
116    /// If this is None, MIME type is inferred from the extension of ***relative_path***
117    /// and if that fails, `application/octet-stream` is used.  
118    /// 
119    /// # Support
120    /// Android 10 (API level 29) or higher.  
121    ///
122    /// Note :  
123    /// - [`PublicAudioDir::Audiobooks`] is not available on Android 9 (API level 28) and lower.
124    /// Availability on a given device can be verified by calling [`PublicStorage::is_audiobooks_dir_available`].  
125    /// - [`PublicAudioDir::Recordings`] is not available on Android 11 (API level 30) and lower.
126    /// Availability on a given device can be verified by calling [`PublicStorage::is_recordings_dir_available`].  
127    /// - Others dirs are available in all Android versions.
128    pub fn create_new_file(
129        &self,
130        volume_id: Option<&StorageVolumeId>,
131        base_dir: impl Into<PublicDir>,
132        relative_path: impl AsRef<std::path::Path>, 
133        mime_type: Option<&str>
134    ) -> crate::Result<FileUri> {
135
136        on_android!({
137            impl_se!(struct Req<'a> { media_store_volume_name: &'a str, relative_path: std::path::PathBuf, mime_type: Option<&'a str> });
138            impl_de!(struct Res { uri: FileUri });
139
140            if self.0.api_level()? < api_level::ANDROID_10 {
141                return Err(Error { msg: "requires Android 10 (API level 29) or higher".into() })
142            }
143
144            let consts = self.0.consts()?;
145            let relative_path = {
146                let mut p = std::path::PathBuf::new();
147                p.push(consts.public_dir_name(base_dir)?);
148                p.push(validate_relative_path(relative_path.as_ref())?);
149                p
150            };
151            let media_store_volume_name = volume_id
152                .map(|v| v.media_store_volume_name.as_ref())
153                .unwrap_or(consts.media_store_primary_volume_name.as_ref())
154                .ok_or_else(|| Error::with("The storage volume is not available for PublicStorage"))?;
155
156            self.0.api
157                .run_mobile_plugin::<Res>("createNewMediaStoreFile", Req {
158                    media_store_volume_name, 
159                    relative_path,
160                    mime_type,
161                })
162                .map(|v| v.uri)
163                .map_err(Into::into)
164        })
165    }
166
167    /// Recursively create a directory and all of its parent components if they are missing.  
168    /// If it already exists, do nothing.
169    /// 
170    /// [`PublicStorage::create_new_file`] does this automatically, so there is no need to use it together.
171    /// 
172    /// # Args  
173    /// - ***volume_id*** :  
174    /// ID of the storage volume, such as internal storage, SD card, etc.  
175    /// If `None` is provided, [`the primary storage volume`](PublicStorage::get_primary_volume) will be used.  
176    /// 
177    /// - ***base_dir*** :  
178    /// The base directory.  
179    ///  
180    /// - ***relative_path*** :  
181    /// The directory path relative to the base directory.    
182    /// The system may sanitize these strings as needed, so those strings may not be used as it is.
183    ///  
184    /// # Support
185    /// Android 10 (API level 29) or higher.  
186    ///
187    /// Note :  
188    /// - [`PublicAudioDir::Audiobooks`] is not available on Android 9 (API level 28) and lower.
189    /// Availability on a given device can be verified by calling [`PublicStorage::is_audiobooks_dir_available`].  
190    /// - [`PublicAudioDir::Recordings`] is not available on Android 11 (API level 30) and lower.
191    /// Availability on a given device can be verified by calling [`PublicStorage::is_recordings_dir_available`].  
192    /// - Others dirs are available in all Android versions.
193    pub fn create_dir_all(
194        &self,
195        volume_id: Option<&StorageVolumeId>,
196        base_dir: impl Into<PublicDir>,
197        relative_path: impl AsRef<std::path::Path>, 
198    ) -> Result<()> {
199
200        on_android!({
201            if self.0.api_level()? < api_level::ANDROID_10 {
202                return Err(Error { msg: "requires Android 10 (API level 29) or higher".into() })
203            }
204
205            let relative_path = validate_relative_path(relative_path.as_ref())?;
206            let base_dir = base_dir.into();
207
208            let tmp_file_uri = self.create_new_file(
209                volume_id,
210                base_dir, 
211                relative_path.join("TMP-01K3CGCKYSAQ1GHF8JW5FGD4RW"), 
212                Some(match base_dir {
213                    PublicDir::Image(_) => "image/png",
214                    PublicDir::Audio(_) => "audio/mp3",
215                    PublicDir::Video(_) => "video/mp4",
216                    PublicDir::GeneralPurpose(_) => "application/octet-stream"
217                })
218            )?;
219            let _ = self.0.remove_file(&tmp_file_uri);
220
221            Ok(())
222        })
223    }
224
225    /// Retrieves the absolute path for a specified public directory within the given storage volume.   
226    /// This function does **not** create any directories; it only constructs the path.
227    /// 
228    /// **Please avoid using this whenever possible.**    
229    /// Use it only in cases that cannot be handled by [`PublicStorage::create_new_file`] or [`PrivateStorage::resolve_path`], 
230    /// such as when you need to pass the absolute path of a user-accessible file as an argument to any database library, debug logger, and etc.  
231    ///
232    /// Since **Android 11** (not Android 10),
233    /// you can create files and folders under this directory and read or write **only** them.  
234    /// If not, you can do nothing with this path.
235    /// 
236    /// When using [`PublicImageDir`], use only image type for file name extension, 
237    /// using other type extension or none may cause errors.
238    /// Similarly, use only the corresponding extesions for [`PublicVideoDir`] and [`PublicAudioDir`].
239    /// Only [`PublicGeneralPurposeDir`] supports all extensions and no extension. 
240    /// 
241    /// # Note
242    /// Filesystem access via this path may be heavily impacted by emulation overhead.
243    /// And those files will not be registered in MediaStore. 
244    /// It might eventually be registered over time, but this should not be expected.
245    /// As a result, it may not appear in gallery apps or photo picker tools.
246    /// 
247    /// You cannot access files created by other apps. 
248    /// Additionally, if the app is uninstalled, 
249    /// you will no longer be able to access the files you created, 
250    /// even if the app is reinstalled.  
251    /// Android tends to restrict public file access using paths, so this may stop working in the future.
252    /// 
253    /// # Args
254    /// - ***volume_id*** :  
255    /// ID of the storage volume, such as internal storage, SD card, etc.  
256    /// If `None` is provided, [`the primary storage volume`](PublicStorage::get_primary_volume) will be used.  
257    /// 
258    /// - ***base_dir*** :  
259    /// The base directory.  
260    ///  
261    /// # Support
262    /// Android 10 (API level 29) or higher.  
263    /// 
264    /// Note :  
265    /// - [`PublicAudioDir::Audiobooks`] is not available on Android 9 (API level 28) and lower.
266    /// Availability on a given device can be verified by calling [`PublicStorage::is_audiobooks_dir_available`].  
267    /// - [`PublicAudioDir::Recordings`] is not available on Android 11 (API level 30) and lower.
268    /// Availability on a given device can be verified by calling [`PublicStorage::is_recordings_dir_available`].  
269    /// - Others dirs are available in all Android versions.
270    pub fn resolve_path(
271        &self,
272        volume_id: Option<&StorageVolumeId>,
273        base_dir: impl Into<PublicDir>,
274    ) -> Result<std::path::PathBuf> {
275
276        if self.0.api_level()? < api_level::ANDROID_10 {
277            return Err(Error { msg: "requires Android 10 (API level 29) or higher".into() })
278        }
279
280        let mut path = match volume_id {
281            Some(volume_id) => {
282                let (vn, tp) = volume_id.media_store_volume_name.as_ref()
283                    .zip(volume_id.top_directory_path.as_ref())
284                    .ok_or_else(|| Error::with("The storage volume is not available for PublicStorage"))?;
285                
286                if !self.0.check_media_store_volume_name_available(vn)? {
287                    return Err(Error::with("The storage volume is not currently available"))
288                }
289
290                tp.clone()
291            },
292            None => {
293                self.get_primary_volume()?
294                    .and_then(|v| v.id.top_directory_path)
295                    .ok_or_else(|| Error::with("Primary storage volume is not currently available"))?
296            }
297        };
298
299        path.push(self.0.consts()?.public_dir_name(base_dir)?);
300        Ok(path)
301    }
302
303    /// Create the specified directory URI that has **no permissions**.  
304    /// 
305    /// This should only be used as `initial_location` in the file picker. 
306    /// It must not be used for any other purpose.  
307    /// 
308    /// This is useful when selecting save location, 
309    /// but when selecting existing entries, `initial_location` is often better with None.
310    /// 
311    /// # Args  
312    /// - ***volume_id*** :  
313    /// ID of the storage volume, such as internal storage, SD card, etc.  
314    /// If `None` is provided, [`the primary storage volume`](PublicStorage::get_primary_volume) will be used.  
315    /// 
316    /// - ***base_dir*** :  
317    /// The base directory.  
318    ///  
319    /// - ***relative_path*** :  
320    /// The directory path relative to the base directory.    
321    ///  
322    /// # Support
323    /// If use `None` to ***volume***, all Android version: 
324    /// otherwise: Android 10 (API level 29) or higher
325    ///
326    /// Note :  
327    /// - [`PublicAudioDir::Audiobooks`] is not available on Android 9 (API level 28) and lower.
328    /// Availability on a given device can be verified by calling [`PublicStorage::is_audiobooks_dir_available`].  
329    /// - [`PublicAudioDir::Recordings`] is not available on Android 11 (API level 30) and lower.
330    /// Availability on a given device can be verified by calling [`PublicStorage::is_recordings_dir_available`].  
331    /// - Others dirs are available in all Android versions.
332    pub fn resolve_initial_location(
333        &self,
334        volume_id: Option<&StorageVolumeId>,
335        base_dir: impl Into<PublicDir>,
336        relative_path: impl AsRef<std::path::Path>,
337        create_dir_all: bool
338    ) -> Result<FileUri> {
339
340        on_android!({
341            let base_dir = base_dir.into();
342            
343            let mut uri = self.resolve_initial_location_top(volume_id)?;
344            uri.uri.push_str(self.0.consts()?.public_dir_name(base_dir)?);
345
346            let relative_path = validate_relative_path(relative_path.as_ref())?;
347            let relative_path = relative_path.to_string_lossy();
348            if !relative_path.is_empty() {
349                uri.uri.push_str("%2F");
350                uri.uri.push_str(&encode_document_id(relative_path.as_ref()));
351            }
352
353            if create_dir_all {
354                let _ = self.create_dir_all(volume_id, base_dir, relative_path.as_ref());
355            }
356
357            Ok(uri)
358        })
359    }
360
361    /// Create the specified directory URI that has **no permissions**.  
362    /// 
363    /// This should only be used as `initial_location` in the file picker. 
364    /// It must not be used for any other purpose.  
365    /// 
366    /// This is useful when selecting save location, 
367    /// but when selecting existing entries, `initial_location` is often better with None.
368    /// 
369    /// # Args  
370    /// - ***volume_id*** :  
371    /// ID of the storage volume, such as internal storage, SD card, etc.  
372    /// If `None` is provided, [`the primary storage volume`](PublicStorage::get_primary_volume) will be used.  
373    /// 
374    /// # Support
375    /// All Android version: 
376    ///
377    /// Note :  
378    /// - [`PublicAudioDir::Audiobooks`] is not available on Android 9 (API level 28) and lower.
379    /// Availability on a given device can be verified by calling [`PublicStorage::is_audiobooks_dir_available`].  
380    /// - [`PublicAudioDir::Recordings`] is not available on Android 11 (API level 30) and lower.
381    /// Availability on a given device can be verified by calling [`PublicStorage::is_recordings_dir_available`].  
382    /// - Others dirs are available in all Android versions.
383    pub fn resolve_initial_location_top(
384        &self,
385        volume_id: Option<&StorageVolumeId>
386    ) -> Result<FileUri> {
387
388        on_android!({
389            let volume_id = volume_id
390                .and_then(|v| v.uuid.as_deref())
391                .unwrap_or("primary");
392
393            Ok(FileUri {
394                uri: format!("content://com.android.externalstorage.documents/document/{volume_id}%3A"),
395                document_top_tree_uri: None 
396            })
397        })
398    }
399
400    /// Verify whether the basic functions of PublicStorage 
401    /// (such as [`PublicStorage::create_new_file`]) can be performed.
402    /// 
403    /// If on Android 9 (API level 28) and lower, this returns false.  
404    /// If on Android 10 (API level 29) or higher, this returns true.  
405    /// 
406    /// # Support
407    /// All Android version.
408    pub fn is_available(&self) -> crate::Result<bool> {
409        Ok(api_level::ANDROID_10 <= self.0.api_level()?)
410    }
411
412    /// Verify whether [`PublicAudioDir::Audiobooks`] is available on a given device.   
413    /// 
414    /// If on Android 9 (API level 28) and lower, this returns false.  
415    /// If on Android 10 (API level 29) or higher, this returns true.  
416    /// 
417    /// # Support
418    /// All Android version.
419    pub fn is_audiobooks_dir_available(&self) -> crate::Result<bool> {
420        Ok(self.0.consts()?.env_dir_audiobooks.is_some())
421    }
422
423    /// Verify whether [`PublicAudioDir::Recordings`] is available on a given device.   
424    /// 
425    /// If on Android 11 (API level 30) and lower, this returns false.  
426    /// If on Android 12 (API level 31) or higher, this returns true.  
427    /// 
428    /// # Support
429    /// All Android version.
430    pub fn is_recordings_dir_available(&self) -> crate::Result<bool> {
431        Ok(self.0.consts()?.env_dir_recordings.is_some())
432    }
433}