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}