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}