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}