Skip to content

Commit bcdd510

Browse files
lucasfernogVulnX
andauthored
feat(core): resolve file names from Android content URIs (#13012)
* feat(core): resolve file names from Android content URIs This PR adds a new Android path plugin function to resolve file names from content URIs. `PathResolver::file_name` was added to expose this API on Rust, and the existing `@tauri-apps/api/path` basename and extname function now leverages it on Android. Closes tauri-apps/plugins-workspace#1775 Tauri core port from tauri-apps/plugins-workspace#2421 Co-authored-by: VulnX * update change file [skip ci] Co-authored-by: VulnX <[email protected]> --------- Co-authored-by: VulnX <[email protected]>
1 parent 71cb1e2 commit bcdd510

File tree

6 files changed

+123
-11
lines changed

6 files changed

+123
-11
lines changed
+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"tauri": minor:feat
3+
"@tauri-apps/api": minor:feat
4+
---
5+
6+
The `path` basename and extname APIs now accept Android content URIs, such as the paths returned by the dialog plugin.

.changes/path-file-name-android.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"tauri": minor:feat
3+
---
4+
5+
Added `PathResolver::file_name` to resolve file names from content URIs on Android (leverating `std::path::Path::file_name` on other platforms).

crates/tauri/mobile/android/src/main/java/app/tauri/PathPlugin.kt

+39
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,25 @@
55
package app.tauri
66

77
import android.app.Activity
8+
import android.database.Cursor
9+
import android.net.Uri
810
import android.os.Build
911
import android.os.Environment
12+
import android.provider.OpenableColumns
1013
import app.tauri.annotation.Command
14+
import app.tauri.annotation.InvokeArg
1115
import app.tauri.annotation.TauriPlugin
1216
import app.tauri.plugin.Plugin
1317
import app.tauri.plugin.Invoke
1418
import app.tauri.plugin.JSObject
1519

1620
const val TAURI_ASSETS_DIRECTORY_URI = "asset://localhost/"
1721

22+
@InvokeArg
23+
class GetFileNameFromUriArgs {
24+
lateinit var uri: String
25+
}
26+
1827
@TauriPlugin
1928
class PathPlugin(private val activity: Activity): Plugin(activity) {
2029
private fun resolvePath(invoke: Invoke, path: String?) {
@@ -23,6 +32,15 @@ class PathPlugin(private val activity: Activity): Plugin(activity) {
2332
invoke.resolve(obj)
2433
}
2534

35+
@Command
36+
fun getFileNameFromUri(invoke: Invoke) {
37+
val args = invoke.parseArgs(GetFileNameFromUriArgs::class.java)
38+
val name = getRealNameFromURI(activity, Uri.parse(args.uri))
39+
val res = JSObject()
40+
res.put("name", name)
41+
invoke.resolve(res)
42+
}
43+
2644
@Command
2745
fun getAudioDir(invoke: Invoke) {
2846
resolvePath(invoke, activity.getExternalFilesDir(Environment.DIRECTORY_MUSIC)?.absolutePath)
@@ -91,3 +109,24 @@ class PathPlugin(private val activity: Activity): Plugin(activity) {
91109
resolvePath(invoke, Environment.getExternalStorageDirectory().absolutePath)
92110
}
93111
}
112+
113+
fun getRealNameFromURI(activity: Activity, contentUri: Uri): String? {
114+
var cursor: Cursor? = null
115+
try {
116+
val projection = arrayOf(OpenableColumns.DISPLAY_NAME)
117+
cursor = activity.contentResolver.query(contentUri, projection, null, null, null)
118+
119+
cursor?.let {
120+
val columnIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
121+
if (it.moveToFirst()) {
122+
return it.getString(columnIndex)
123+
}
124+
}
125+
} catch (e: Exception) {
126+
Logger.error("failed to get real name from URI $e")
127+
} finally {
128+
cursor?.close()
129+
}
130+
131+
return null // Return null if no file name could be resolved
132+
}

crates/tauri/src/path/android.rs

+44-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44

55
use super::Result;
66
use crate::{plugin::PluginHandle, Runtime};
7-
use std::path::PathBuf;
7+
use std::{
8+
ffi::OsStr,
9+
path::{Path, PathBuf},
10+
};
811

912
/// A helper class to access the mobile path APIs.
1013
pub struct PathResolver<R: Runtime>(pub(crate) PluginHandle<R>);
@@ -20,7 +23,47 @@ struct PathResponse {
2023
path: PathBuf,
2124
}
2225

26+
#[derive(serde::Serialize)]
27+
struct GetFileNameFromUriRequest<'a> {
28+
uri: &'a str,
29+
}
30+
31+
#[derive(serde::Deserialize)]
32+
struct GetFileNameFromUriResponse {
33+
name: Option<String>,
34+
}
35+
2336
impl<R: Runtime> PathResolver<R> {
37+
/// Returns the final component of the `Path`, if there is one.
38+
///
39+
/// If the path is a normal file, this is the file name. If it's the path of a directory, this
40+
/// is the directory name.
41+
///
42+
/// Returns [`None`] if the path terminates in `..`.
43+
///
44+
/// On Android this also supports checking the file name of content URIs, such as the values returned by the dialog plugin.
45+
///
46+
/// If you are dealing with plain file system paths or not worried about Android content URIs, prefer [`Path::file_name`].
47+
pub fn file_name(&self, path: &str) -> Option<String> {
48+
if path.starts_with("content://") || path.starts_with("file://") {
49+
self
50+
.0
51+
.run_mobile_plugin::<GetFileNameFromUriResponse>(
52+
"getFileNameFromUri",
53+
GetFileNameFromUriRequest { uri: path },
54+
)
55+
.map(|r| r.name)
56+
.unwrap_or_else(|e| {
57+
log::error!("failed to get file name from URI: {e}");
58+
None
59+
})
60+
} else {
61+
Path::new(path)
62+
.file_name()
63+
.map(|name| name.to_string_lossy().into_owned())
64+
}
65+
}
66+
2467
fn call_resolve(&self, dir: &str) -> Result<PathBuf> {
2568
self
2669
.0

crates/tauri/src/path/desktop.rs

+17-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
use super::{Error, Result};
66
use crate::{AppHandle, Manager, Runtime};
7-
use std::path::PathBuf;
7+
use std::path::{Path, PathBuf};
88

99
/// The path resolver is a helper class for general and application-specific path APIs.
1010
pub struct PathResolver<R: Runtime>(pub(crate) AppHandle<R>);
@@ -16,6 +16,22 @@ impl<R: Runtime> Clone for PathResolver<R> {
1616
}
1717

1818
impl<R: Runtime> PathResolver<R> {
19+
/// Returns the final component of the `Path`, if there is one.
20+
///
21+
/// If the path is a normal file, this is the file name. If it's the path of a directory, this
22+
/// is the directory name.
23+
///
24+
/// Returns [`None`] if the path terminates in `..`.
25+
///
26+
/// On Android this also supports checking the file name of content URIs, such as the values returned by the dialog plugin.
27+
///
28+
/// If you are dealing with plain file system paths or not worried about Android content URIs, prefer [`Path::file_name`].
29+
pub fn file_name(&self, path: &str) -> Option<String> {
30+
Path::new(path)
31+
.file_name()
32+
.map(|name| name.to_string_lossy().into_owned())
33+
}
34+
1935
/// Returns the path to the user's audio directory.
2036
///
2137
/// ## Platform-specific

crates/tauri/src/path/plugin.rs

+12-9
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,9 @@ pub fn dirname(path: String) -> Result<PathBuf> {
169169
}
170170

171171
#[command(root = "crate")]
172-
pub fn extname(path: String) -> Result<String> {
173-
match Path::new(&path)
172+
pub fn extname<R: Runtime>(app: AppHandle<R>, path: String) -> Result<String> {
173+
let file_name = app.path().file_name(&path).ok_or(Error::NoExtension)?;
174+
match Path::new(&file_name)
174175
.extension()
175176
.and_then(std::ffi::OsStr::to_str)
176177
{
@@ -180,8 +181,8 @@ pub fn extname(path: String) -> Result<String> {
180181
}
181182

182183
#[command(root = "crate")]
183-
pub fn basename(path: &str, ext: Option<&str>) -> Result<String> {
184-
let file_name = Path::new(path).file_name().map(|f| f.to_string_lossy());
184+
pub fn basename<R: Runtime>(app: AppHandle<R>, path: &str, ext: Option<&str>) -> Result<String> {
185+
let file_name = app.path().file_name(path);
185186
match file_name {
186187
Some(p) => {
187188
let maybe_stripped = if let Some(ext) = ext {
@@ -251,36 +252,38 @@ pub(crate) fn init<R: Runtime>() -> TauriPlugin<R> {
251252

252253
#[cfg(test)]
253254
mod tests {
255+
use crate::test::mock_app;
254256

255257
#[test]
256258
fn basename() {
259+
let app = mock_app();
257260
let path = "/path/to/some-json-file.json";
258261
assert_eq!(
259-
super::basename(path, Some(".json")).unwrap(),
262+
super::basename(app.handle().clone(), path, Some(".json")).unwrap(),
260263
"some-json-file"
261264
);
262265

263266
let path = "/path/to/some-json-file.json";
264267
assert_eq!(
265-
super::basename(path, Some("json")).unwrap(),
268+
super::basename(app.handle().clone(), path, Some("json")).unwrap(),
266269
"some-json-file."
267270
);
268271

269272
let path = "/path/to/some-json-file.html.json";
270273
assert_eq!(
271-
super::basename(path, Some(".json")).unwrap(),
274+
super::basename(app.handle().clone(), path, Some(".json")).unwrap(),
272275
"some-json-file.html"
273276
);
274277

275278
let path = "/path/to/some-json-file.json.json";
276279
assert_eq!(
277-
super::basename(path, Some(".json")).unwrap(),
280+
super::basename(app.handle().clone(), path, Some(".json")).unwrap(),
278281
"some-json-file.json"
279282
);
280283

281284
let path = "/path/to/some-json-file.json.html";
282285
assert_eq!(
283-
super::basename(path, Some(".json")).unwrap(),
286+
super::basename(app.handle().clone(), path, Some(".json")).unwrap(),
284287
"some-json-file.json.html"
285288
);
286289
}

0 commit comments

Comments
 (0)