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 if self.0.api_level()? < api_level::ANDROID_10 {
45 return Err(Error { msg: "requires Android 10 (API level 29) or higher".into() })
46 }
47
48 let mut volumes = self.0.api
49 .run_mobile_plugin::<Res>("getStorageVolumes", "")
50 .map(|v| v.volumes.into_iter())
51 .map(|v| v.map(|v|
52 PublicStorageVolume {
53 description: v.description.unwrap_or_else(|| v.volume_name.clone()),
54 id: PublicStorageVolumeId(v.volume_name),
55 is_primary: v.is_primary,
56 }
57 ))
58 .map(|v| v.collect::<Vec<_>>())?;
59
60 // primary volume を先頭にする。他はそのままの順序
61 volumes.sort_by(|a, b| b.is_primary.cmp(&a.is_primary));
62
63 Ok(volumes)
64 })
65 }
66
67 /// Gets a primary storage volume.
68 /// This is the most common and recommended storage volume for placing files that can be accessed by other apps or user.
69 ///
70 /// A device always has one (and one only) primary storage volume.
71 ///
72 /// The returned primary volume may not currently be accessible
73 /// if it has been mounted by the user on their computer,
74 /// has been removed from the device, or some other problem has happened.
75 ///
76 /// You can find a list of all currently available volumes using [`PublicStorage::get_available_volumes`].
77 ///
78 /// # Note
79 /// The volume represents the logical view of a storage volume for an individual user:
80 /// each user may have a different view for the same physical volume
81 /// (e.g. when the volume is a built-in emulated storage).
82 ///
83 /// The primary volume provides a separate area for each user in a multi-user environment.
84 ///
85 /// # Support
86 /// Android 10 (API level 29) or higher.
87 ///
88 /// # References
89 /// <https://developer.android.com/reference/android/provider/MediaStore#VOLUME_EXTERNAL_PRIMARY>
90 pub fn get_primary_volume(&self) -> Result<PublicStorageVolume> {
91 on_android!({
92 impl_de!(struct Res { volume_name: String, description: Option<String>, is_primary: bool });
93
94 if self.0.api_level()? < api_level::ANDROID_10 {
95 return Err(Error { msg: "requires Android 10 (API level 29) or higher".into() })
96 }
97
98 self.0.api
99 .run_mobile_plugin::<Res>("getPrimaryStorageVolume", "")
100 .map(|v|
101 PublicStorageVolume {
102 description: v.description.unwrap_or_else(|| v.volume_name.clone()),
103 id: PublicStorageVolumeId(v.volume_name),
104 is_primary: v.is_primary,
105 }
106 )
107 .map_err(Into::into)
108 })
109 }
110
111 /// Creates a new empty file in the specified public directory of the storage volume.
112 /// This returns a **persistent read-write** URI.
113 ///
114 /// The created file has the following features:
115 /// - It is registered with the appropriate MediaStore as needed.
116 /// - The app can fully manage it until the app is uninstalled.
117 /// - It is **not** removed when the app itself is uninstalled.
118 ///
119 /// # Args
120 /// - ***volume*** :
121 /// The storage volume, such as internal storage, SD card, etc.
122 /// Usually, you don't need to specify this unless there is a special reason.
123 /// If `None` is provided, [`the primary storage volume`](PublicStorage::get_primary_volume) will be used.
124 ///
125 /// - ***base_dir*** :
126 /// The base directory.
127 /// When using [`PublicImageDir`], use only image MIME types for ***mime_type***, which is discussed below.; using other types may cause errors.
128 /// Similarly, use only the corresponding media types for [`PublicVideoDir`] and [`PublicAudioDir`].
129 /// Only [`PublicGeneralPurposeDir`] supports all MIME types.
130 ///
131 /// - ***use_app_dir*** :
132 /// Determines whether to insert a directory named after the application name
133 /// specified in Tauri's configuration between ***base_dir*** and ***relative_path***.
134 /// It is recommended to enable this unless there is a special reason not to.
135 /// See [`PublicStorage::app_dir_name`]
136 ///
137 /// - ***relative_path*** :
138 /// The file path relative to the base directory.
139 /// Any missing subdirectories in the specified path will be created automatically.
140 /// If a file with the same name already exists,
141 /// the system append a sequential number to ensure uniqueness.
142 /// If no extension is present,
143 /// the system may infer one from ***mime_type*** and may append it to the file name.
144 /// But this append-extension operation depends on the model and version.
145 /// The system may sanitize these strings as needed, so those strings may not be used as it is.
146 ///
147 /// - ***mime_type*** :
148 /// The MIME type of the file to be created.
149 /// If this is None, MIME type is inferred from the extension of ***relative_path***
150 /// and if that fails, `application/octet-stream` is used.
151 ///
152 /// # Support
153 /// Android 10 (API level 29) or higher.
154 ///
155 /// Note :
156 /// - [`PublicAudioDir::Audiobooks`] is not available on Android 9 (API level 28) and lower.
157 /// Availability on a given device can be verified by calling [`PublicStorage::is_audiobooks_dir_available`].
158 /// - [`PublicAudioDir::Recordings`] is not available on Android 11 (API level 30) and lower.
159 /// Availability on a given device can be verified by calling [`PublicStorage::is_recordings_dir_available`].
160 /// - Others dirs are available in all Android versions.
161 pub fn create_new_file(
162 &self,
163 volume: Option<&PublicStorageVolumeId>,
164 base_dir: impl Into<PublicDir>,
165 use_app_dir: bool,
166 relative_path: impl AsRef<std::path::Path>,
167 mime_type: Option<&str>
168 ) -> crate::Result<FileUri> {
169
170 on_android!({
171 if self.0.api_level()? < api_level::ANDROID_10 {
172 return Err(Error { msg: "requires Android 10 (API level 29) or higher".into() })
173 }
174
175 let base_dir = base_dir.into();
176 let relative_path = validate_relative_path(relative_path.as_ref())?;
177 let base_dir_uri = self.get_dir_uri(volume, base_dir)?;
178
179 let relative_path = {
180 let mut p = std::path::PathBuf::new();
181 p.push(self.get_dir_name(base_dir)?);
182 if use_app_dir {
183 p.push(self.app_dir_name()?);
184 }
185 p.push(relative_path);
186 p
187 };
188
189 self.0.create_new_file(&base_dir_uri, relative_path, mime_type)
190 })
191 }
192
193 /// Recursively create a directory and all of its parent components if they are missing.
194 /// If it already exists, do nothing.
195 ///
196 /// [`PublicStorage::create_new_file`] does this automatically, so there is no need to use it together.
197 ///
198 /// # Args
199 /// - ***volume*** :
200 /// The storage volume, such as internal storage, SD card, etc.
201 /// If `None` is provided, [`the primary storage volume`](PublicStorage::get_primary_volume) will be used.
202 ///
203 /// - ***base_dir*** :
204 /// The base directory.
205 ///
206 /// - ***use_app_dir*** :
207 /// Determines whether to insert a directory named after the application name
208 /// specified in Tauri's configuration between ***base_dir*** and ***relative_path***.
209 ///
210 /// - ***relative_path*** :
211 /// The directory path relative to the base directory.
212 /// The system may sanitize these strings as needed, so those strings may not be used as it is.
213 ///
214 /// # Support
215 /// Android 10 (API level 29) or higher.
216 ///
217 /// Note :
218 /// - [`PublicAudioDir::Audiobooks`] is not available on Android 9 (API level 28) and lower.
219 /// Availability on a given device can be verified by calling [`PublicStorage::is_audiobooks_dir_available`].
220 /// - [`PublicAudioDir::Recordings`] is not available on Android 11 (API level 30) and lower.
221 /// Availability on a given device can be verified by calling [`PublicStorage::is_recordings_dir_available`].
222 /// - Others dirs are available in all Android versions.
223 pub fn create_dir_all(
224 &self,
225 volume: Option<&PublicStorageVolumeId>,
226 base_dir: impl Into<PublicDir>,
227 use_app_dir: bool,
228 relative_path: impl AsRef<std::path::Path>,
229 ) -> Result<()> {
230
231 on_android!({
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 relative_path = validate_relative_path(relative_path.as_ref())?;
237 let base_dir = base_dir.into();
238
239 let tmp_file_uri = self.create_new_file(
240 volume,
241 base_dir,
242 use_app_dir,
243 relative_path.join("TMP-01K3CGCKYSAQ1GHF8JW5FGD4RW"),
244 Some(match base_dir {
245 PublicDir::Image(_) => "image/png",
246 PublicDir::Audio(_) => "audio/mp3",
247 PublicDir::Video(_) => "video/mp4",
248 PublicDir::GeneralPurpose(_) => "application/octet-stream"
249 })
250 )?;
251 let _ = self.0.remove_file(&tmp_file_uri);
252
253 Ok(())
254 })
255 }
256
257 /// Retrieves the absolute path for a specified public directory within the given storage volume.
258 /// This function does **not** create any directories; it only constructs the path.
259 ///
260 /// You can create files and folders under this directory and read or write only them.
261 ///
262 /// **Please avoid using this whenever possible.**
263 /// Use it only in cases that cannot be handled by [`PublicStorage::create_new_file`] or [`PrivateStorage::resolve_path`],
264 /// such as when you need to pass the absolute path of a user-accessible file as an argument to any database library.
265 ///
266 /// # Note
267 /// Filesystem access via this path may be heavily impacted by emulation overhead.
268 /// And those files will not be registered in MediaStore.
269 /// It might eventually be registered over time, but this should not be expected.
270 /// As a result, it may not appear in gallery apps or photo picker tools.
271 ///
272 /// You cannot access files created by other apps.
273 /// Additionally, if the app is uninstalled,
274 /// you will no longer be able to access the files you created,
275 /// even if the app is reinstalled.
276 /// Android tends to restrict public file access using paths, so this may stop working in the future.
277 ///
278 /// # Args
279 /// - ***volume*** :
280 /// The storage volume, such as internal storage, SD card, etc.
281 /// If `None` is provided, [`the primary storage volume`](PublicStorage::get_primary_volume) will be used.
282 ///
283 /// - ***base_dir*** :
284 /// The base directory.
285 ///
286 /// - ***use_app_dir*** :
287 /// Determines whether to insert a directory named after the application name
288 /// specified in Tauri's configuration under ***base_dir***.
289 ///
290 /// # Support
291 /// Android 10 (API level 29) or higher.
292 ///
293 /// Note :
294 /// - [`PublicAudioDir::Audiobooks`] is not available on Android 9 (API level 28) and lower.
295 /// Availability on a given device can be verified by calling [`PublicStorage::is_audiobooks_dir_available`].
296 /// - [`PublicAudioDir::Recordings`] is not available on Android 11 (API level 30) and lower.
297 /// Availability on a given device can be verified by calling [`PublicStorage::is_recordings_dir_available`].
298 /// - Others dirs are available in all Android versions.
299 pub fn resolve_path(
300 &self,
301 volume: Option<&PublicStorageVolumeId>,
302 base_dir: impl Into<PublicDir>,
303 use_app_dir: bool,
304 ) -> Result<std::path::PathBuf> {
305
306 on_android!({
307 if self.0.api_level()? < api_level::ANDROID_10 {
308 return Err(Error::with("requires Android 10 (API level 29) or higher"))
309 }
310
311 let mut path = self.get_volume_path(volume)?;
312 path.push(self.get_dir_name(base_dir)?);
313 if use_app_dir {
314 path.push(self.app_dir_name()?);
315 }
316 Ok(path)
317 })
318 }
319
320 /// Create the specified directory URI that has **no permissions**.
321 ///
322 /// This should only be used as `initial_location` in the file picker.
323 /// It must not be used for any other purpose.
324 ///
325 /// This is useful when selecting save location,
326 /// but when selecting existing entries, `initial_location` is often better with None.
327 ///
328 /// # Args
329 /// - ***volume*** :
330 /// The storage volume, such as internal storage, SD card, etc.
331 /// If `None` is provided, [`the primary storage volume`](PublicStorage::get_primary_volume) will be used.
332 ///
333 /// - ***base_dir*** :
334 /// The base directory.
335 ///
336 /// - ***use_app_dir*** :
337 /// Determines whether to insert a directory named after the application name
338 /// specified in Tauri's configuration between ***base_dir*** and ***relative_path***.
339 ///
340 /// - ***relative_path*** :
341 /// The directory path relative to the base directory.
342 /// The system may sanitize these strings as needed, so those strings may not be used as it is.
343 ///
344 /// # Support
345 /// If use `None` to ***volume***, all Android version:
346 /// otherwise: 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 resolve_initial_location(
355 &self,
356 volume: Option<&PublicStorageVolumeId>,
357 base_dir: impl Into<PublicDir>,
358 use_app_dir: bool,
359 relative_path: impl AsRef<std::path::Path>,
360 create_dir_all: bool
361 ) -> Result<FileUri> {
362
363 on_android!({
364 let base_dir = base_dir.into();
365
366 let mut uri = self.resolve_initial_location_top(volume)?;
367 uri.uri.push_str(self.get_dir_name(base_dir)?);
368
369 let relative_path = validate_relative_path(relative_path.as_ref())?;
370 let relative_path = relative_path.to_string_lossy();
371 if !relative_path.is_empty() {
372 uri.uri.push_str("%2F");
373 uri.uri.push_str(&match use_app_dir {
374 false => encode_document_id(relative_path.as_ref()),
375 true => {
376 let mut r = std::path::PathBuf::new();
377 r.push(self.app_dir_name()?);
378 r.push(relative_path.as_ref());
379 encode_document_id(r.to_string_lossy().as_ref())
380 },
381 });
382 }
383
384 if create_dir_all {
385 let _ = self.create_dir_all(volume, base_dir, use_app_dir, relative_path.as_ref());
386 }
387
388 Ok(uri)
389 })
390 }
391
392 /// Create the specified directory URI that has **no permissions**.
393 ///
394 /// This should only be used as `initial_location` in the file picker.
395 /// It must not be used for any other purpose.
396 ///
397 /// This is useful when selecting save location,
398 /// but when selecting existing entries, `initial_location` is often better with None.
399 ///
400 /// # Args
401 /// - ***volume*** :
402 /// The storage volume, such as internal storage, SD card, etc.
403 /// If `None` is provided, [`the primary storage volume`](PublicStorage::get_primary_volume) will be used.
404 ///
405 /// # Support
406 /// If use `None` to ***volume***, all Android version:
407 /// otherwise: Android 10 (API level 29) or higher
408 ///
409 /// Note :
410 /// - [`PublicAudioDir::Audiobooks`] is not available on Android 9 (API level 28) and lower.
411 /// Availability on a given device can be verified by calling [`PublicStorage::is_audiobooks_dir_available`].
412 /// - [`PublicAudioDir::Recordings`] is not available on Android 11 (API level 30) and lower.
413 /// Availability on a given device can be verified by calling [`PublicStorage::is_recordings_dir_available`].
414 /// - Others dirs are available in all Android versions.
415 pub fn resolve_initial_location_top(
416 &self,
417 volume: Option<&PublicStorageVolumeId>
418 ) -> Result<FileUri> {
419
420 on_android!({
421 let volume_uid = match volume {
422 None => None,
423 Some(volume) => {
424 if self.0.api_level()? < api_level::ANDROID_10 {
425 return Err(Error { msg: "requires Android 10 (API level 29) or higher".into() })
426 }
427 self.get_volume_uuid(volume)?
428 }
429 };
430
431 let uri = match volume_uid {
432 None => "content://com.android.externalstorage.documents/document/primary%3A".to_string(),
433 Some(uid) => format!("content://com.android.externalstorage.documents/document/{uid}%3A")
434 };
435
436 Ok(FileUri { uri, document_top_tree_uri: None })
437 })
438 }
439
440 /// Verify whether the basic functions of PublicStorage
441 /// (such as [`PublicStorage::create_new_file`]) can be performed.
442 ///
443 /// If on Android 9 (API level 28) and lower, this returns false.
444 /// If on Android 10 (API level 29) or higher, this returns true.
445 ///
446 /// # Support
447 /// All Android version.
448 pub fn is_available(&self) -> crate::Result<bool> {
449 on_android!({
450 Ok(api_level::ANDROID_10 <= self.0.api_level()?)
451 })
452 }
453
454 /// Verify whether [`PublicAudioDir::Audiobooks`] is available on a given device.
455 ///
456 /// If on Android 9 (API level 28) and lower, this returns false.
457 /// If on Android 10 (API level 29) or higher, this returns true.
458 ///
459 /// # Support
460 /// All Android version.
461 pub fn is_audiobooks_dir_available(&self) -> crate::Result<bool> {
462 on_android!({
463 Ok(api_level::ANDROID_10 <= self.0.api_level()?)
464 })
465 }
466
467 /// Verify whether [`PublicAudioDir::Recordings`] is available on a given device.
468 ///
469 /// If on Android 11 (API level 30) and lower, this returns false.
470 /// If on Android 12 (API level 31) or higher, this returns true.
471 ///
472 /// # Support
473 /// All Android version.
474 pub fn is_recordings_dir_available(&self) -> crate::Result<bool> {
475 on_android!({
476 Ok(api_level::ANDROID_12 <= self.0.api_level()?)
477 })
478 }
479
480 /// Resolve the app dir name from Tauri's config.
481 ///
482 /// This uses "productName" in `src-tauri/tauri.conf.json`
483 ///
484 /// # Support
485 /// All Android version.
486 pub fn app_dir_name(&self) -> crate::Result<&str> {
487 on_android!({
488 use std::sync::OnceLock;
489
490 static APP_DIR_NAME: OnceLock<String> = OnceLock::new();
491
492 if APP_DIR_NAME.get().is_none() {
493 let config = self.0.app.config();
494 let app_name = config.product_name
495 .as_deref()
496 .filter(|s| !s.is_empty())
497 .unwrap_or(&config.identifier)
498 .replace('/', " ");
499
500 let _ = APP_DIR_NAME.set(app_name);
501 }
502
503 Ok(&APP_DIR_NAME.get().expect("Should call 'set' before 'get'"))
504 })
505 }
506}
507
508
509#[allow(unused)]
510impl<'a, R: tauri::Runtime> PublicStorage<'a, R> {
511
512 fn get_volume_uuid(&self, volume: &PublicStorageVolumeId) -> Result<Option<String>> {
513 on_android!({
514 impl_se!(struct Req<'a> { volume_name: &'a str });
515 impl_de!(struct Res { value: Option<String> });
516
517 let volume_name = &volume.0;
518
519 self.0.api
520 .run_mobile_plugin::<Res>("getStorageVolumeUuid", Req { volume_name })
521 .map(|v| v.value)
522 .map_err(Into::into)
523 })
524 }
525
526 fn get_volume_path(&self,
527 volume: Option<&PublicStorageVolumeId>,
528 ) -> Result<std::path::PathBuf> {
529
530 on_android!({
531 impl_se!(struct Req<'a> { volume_name: Option<&'a str> });
532 impl_de!(struct Res { path: String });
533
534 let volume_name = volume.map(|v| v.0.as_str());
535
536 self.0.api
537 .run_mobile_plugin::<Res>("getStorageVolumePath", Req { volume_name })
538 .map(|v| {
539 use std::str::FromStr;
540
541 let r = std::path::PathBuf::from_str(&v.path);
542 std::result::Result::<_, std::convert::Infallible>::unwrap(r)
543 })
544 .map_err(Into::into)
545 })
546 }
547
548 fn get_dir_uri(
549 &self,
550 volume: Option<&PublicStorageVolumeId>,
551 dir: impl Into<PublicDir>,
552 ) -> Result<FileUri> {
553
554 on_android!({
555 impl_se!(struct Req<'a> { dir_type: &'a str, volume_name: Option<&'a str> });
556 impl_de!(struct Res { uri: String });
557
558 let volume_name = volume.map(|v| v.0.as_str());
559 let dir_type = match dir.into() {
560 PublicDir::Image(_) => "Image",
561 PublicDir::Video(_) => "Video",
562 PublicDir::Audio(_) => "Audio",
563 PublicDir::GeneralPurpose(_) => "GeneralPurpose",
564 };
565
566 self.0.api
567 .run_mobile_plugin::<Res>("getPublicDirUri", Req { dir_type, volume_name })
568 .map(|v| FileUri { uri: v.uri, document_top_tree_uri: None })
569 .map_err(Into::into)
570 })
571 }
572
573 fn get_dir_name(&self, dir: impl Into<PublicDir>) -> Result<&str> {
574 on_android!({
575 impl_de!(struct EnvironmentDirs {
576 pictures: String,
577 dcim: String,
578 movies: String,
579 music: String,
580 alarms: String,
581 notifications: String,
582 podcasts: String,
583 ringtones: String,
584 documents: String,
585 download: String,
586 audiobooks: Option<String>,
587 recordings: Option<String>,
588 });
589
590 static DIRS: std::sync::OnceLock<EnvironmentDirs> = std::sync::OnceLock::new();
591
592 if DIRS.get().is_none() {
593 let _ = DIRS.set(
594 self.0.api.run_mobile_plugin::<EnvironmentDirs>("getEnvironmentDirs", "")?
595 );
596 }
597 let e = DIRS.get().expect("Should call 'set' before 'get'");
598
599
600 return Ok(match dir.into() {
601 PublicDir::Image(dir) => match dir {
602 PublicImageDir::Pictures => &e.pictures,
603 PublicImageDir::DCIM => &e.dcim,
604 },
605 PublicDir::Video(dir) => match dir {
606 PublicVideoDir::Movies => &e.movies,
607 PublicVideoDir::DCIM => &e.dcim,
608 },
609 PublicDir::Audio(dir) => match dir {
610 PublicAudioDir::Music => &e.music,
611 PublicAudioDir::Alarms => &e.alarms,
612 PublicAudioDir::Notifications => &e.notifications,
613 PublicAudioDir::Podcasts => &e.podcasts,
614 PublicAudioDir::Ringtones => &e.ringtones,
615 PublicAudioDir::Recordings => e.recordings.as_ref().ok_or_else(|| Error { msg: "requires API level 31 or higher".into() })?,
616 PublicAudioDir::Audiobooks => e.audiobooks.as_ref().ok_or_else(|| Error { msg: "requires API level 29 or higher".into() })?,
617 },
618 PublicDir::GeneralPurpose(dir) => match dir {
619 PublicGeneralPurposeDir::Documents => &e.documents,
620 PublicGeneralPurposeDir::Download => &e.download,
621 }
622 })
623 })
624 }
625}