Skip to content

Commit acff196

Browse files
committed
Fix Android plugin IPC blocked by Tauri ACL system
plugin:name|command IPC from JavaScript is checked against Tauri's ACL (deny-by-default). Custom inline plugins have no defined permissions, so all commands are blocked — causing "Plugin not found" errors even after register_android_plugin succeeds. Add commands/android.rs with pick_file_to_open and take_persistable_uri_permission as regular Tauri commands registered via invoke_handler!. These use PluginHandle::run_mobile_plugin_async which goes through JNI directly, bypassing the ACL entirely. Store the plugin handles in app state from the plugin setup callbacks. Update commands.ts to call invoke('pick_file_to_open') and invoke('take_persistable_uri_permission') instead of the blocked plugin:filePicker|openFile and plugin:uriPermission|takePersistablePermission IPC paths. https://claude.ai/code/session_01J39pMh3ZymXAf9W5K3Ziyi
1 parent 0760891 commit acff196

4 files changed

Lines changed: 115 additions & 21 deletions

File tree

src-tauri/src/commands/android.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright 2024 AppThere
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
//! Android bridge commands for file picking and URI permission persistence.
16+
//!
17+
//! JavaScript cannot call `plugin:name|command` IPC directly because Tauri's
18+
//! ACL system denies unknown plugins by default. These regular Tauri commands
19+
//! (registered via `invoke_handler!`) use `PluginHandle::run_mobile_plugin_async`
20+
//! which goes through JNI directly, bypassing the ACL entirely.
21+
22+
use tauri::Runtime;
23+
24+
/// State holder for the FilePickerPlugin handle (Android only).
25+
pub struct FilePickerHandle<R: Runtime>(pub tauri::plugin::PluginHandle<R>);
26+
27+
/// State holder for the UriPermissionPlugin handle (Android only).
28+
pub struct UriPermissionHandle<R: Runtime>(pub tauri::plugin::PluginHandle<R>);
29+
30+
#[derive(serde::Deserialize)]
31+
struct FilePickerResult {
32+
uri: String,
33+
}
34+
35+
#[derive(serde::Serialize)]
36+
struct TakePersistableArgs {
37+
uri: String,
38+
}
39+
40+
/// Open a file picker using ACTION_OPEN_DOCUMENT (Android).
41+
///
42+
/// Returns the selected `content://` URI as a string. Permissions are
43+
/// persisted inside the Kotlin activity result callback so the file can
44+
/// be reopened from Recents after the app process is killed.
45+
#[cfg(target_os = "android")]
46+
#[tauri::command]
47+
pub async fn pick_file_to_open(
48+
state: tauri::State<'_, FilePickerHandle<tauri::Wry>>,
49+
) -> Result<String, String> {
50+
state
51+
.0
52+
.run_mobile_plugin_async::<FilePickerResult>("openFile", ())
53+
.await
54+
.map(|r| r.uri)
55+
.map_err(|e| e.to_string())
56+
}
57+
58+
/// No-op on non-Android platforms; the desktop file dialog is used instead.
59+
#[cfg(not(target_os = "android"))]
60+
#[tauri::command]
61+
pub async fn pick_file_to_open() -> Result<String, String> {
62+
Err("pick_file_to_open is only available on Android".to_string())
63+
}
64+
65+
/// Persist a `content://` URI permission across app restarts (Android).
66+
///
67+
/// Must be called while the temporary SAF grant is still active (i.e. during
68+
/// the same session in which the URI was obtained from a file picker).
69+
#[cfg(target_os = "android")]
70+
#[tauri::command]
71+
pub async fn take_persistable_uri_permission(
72+
uri: String,
73+
state: tauri::State<'_, UriPermissionHandle<tauri::Wry>>,
74+
) -> Result<(), String> {
75+
state
76+
.0
77+
.run_mobile_plugin_async::<()>("takePersistablePermission", TakePersistableArgs { uri })
78+
.await
79+
.map_err(|e| e.to_string())
80+
}
81+
82+
/// No-op on non-Android platforms.
83+
#[cfg(not(target_os = "android"))]
84+
#[tauri::command]
85+
pub async fn take_persistable_uri_permission(_uri: String) -> Result<(), String> {
86+
Ok(())
87+
}

src-tauri/src/commands/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
pub mod android;
12
pub mod export;
23
pub mod fs;
34
pub mod locale;

src-tauri/src/lib.rs

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
mod commands;
22
mod fonts;
33

4+
use commands::android::{FilePickerHandle, UriPermissionHandle};
5+
46
#[cfg_attr(mobile, tauri::mobile_entry_point)]
57
pub fn run() {
68
eprintln!("DEBUG: run() starting");
@@ -11,18 +13,26 @@ pub fn run() {
1113
.plugin(tauri_plugin_opener::init())
1214
.plugin(
1315
tauri::plugin::Builder::<_, ()>::new("uriPermission")
14-
.setup(|_app, api| {
16+
.setup(|app, api| {
1517
#[cfg(target_os = "android")]
16-
api.register_android_plugin("com.appthere.loki", "UriPermissionPlugin")?;
18+
{
19+
let handle = api
20+
.register_android_plugin("com.appthere.loki", "UriPermissionPlugin")?;
21+
app.manage(UriPermissionHandle(handle));
22+
}
1723
Ok(())
1824
})
1925
.build(),
2026
)
2127
.plugin(
2228
tauri::plugin::Builder::<_, ()>::new("filePicker")
23-
.setup(|_app, api| {
29+
.setup(|app, api| {
2430
#[cfg(target_os = "android")]
25-
api.register_android_plugin("com.appthere.loki", "FilePickerPlugin")?;
31+
{
32+
let handle =
33+
api.register_android_plugin("com.appthere.loki", "FilePickerPlugin")?;
34+
app.manage(FilePickerHandle(handle));
35+
}
2636
Ok(())
2737
})
2838
.build(),
@@ -153,7 +163,9 @@ pub fn run() {
153163
commands::pdf::validate_pdf_x_conformance,
154164
commands::pdf::export_pdf_x,
155165
commands::pdf::validate_text_pdf_x_conformance,
156-
commands::pdf::export_text_pdf_x
166+
commands::pdf::export_text_pdf_x,
167+
commands::android::pick_file_to_open,
168+
commands::android::take_persistable_uri_permission
157169
])
158170
.run(tauri::generate_context!())
159171
.expect("error while running tauri application");

src/lib/tauri/commands.ts

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,27 @@ import type { StyleDefinition, Metadata, LexicalDocumentData } from '../types/od
44
/**
55
* Android only: persist a content:// URI permission across app restarts.
66
*
7-
* After the user picks a file through the SAF file picker, Android grants a
8-
* temporary permission for that URI. Calling this command calls
9-
* `ContentResolver.takePersistableUriPermission()` so the app can still read
10-
* (and write) the file in future sessions — fixing the Recents open-after-
11-
* restart permission error.
12-
*
13-
* The UriPermissionPlugin is registered on the Rust side (lib.rs) so this IPC
14-
* call is properly routed. On non-Android platforms this will reject; callers
15-
* must swallow the error.
7+
* Routes through a regular Rust command (`take_persistable_uri_permission`)
8+
* which calls `PluginHandle::run_mobile_plugin_async` via JNI — bypassing the
9+
* Tauri ACL system that blocks direct `plugin:name|command` IPC from JS.
10+
* No-ops on desktop.
1611
*/
1712
export async function takePersistableUriPermission(uri: string): Promise<void> {
18-
await invoke('plugin:uriPermission|takePersistablePermission', { uri });
13+
await invoke('take_persistable_uri_permission', { uri });
1914
}
2015

21-
2216
/**
2317
* Android only: open a file using ACTION_OPEN_DOCUMENT, which grants a
2418
* persistable content:// URI permission so the file can be reopened from
2519
* the Recents list after the app process is killed.
2620
*
27-
* Returns the selected content:// URI string, or rejects with "cancelled"
28-
* if the user dismissed the picker. Must not be called on desktop (the
29-
* plugin is only registered for Android).
21+
* Routes through a regular Rust command (`pick_file_to_open`) which calls
22+
* `PluginHandle::run_mobile_plugin_async` via JNI — bypassing the Tauri ACL
23+
* system that blocks direct `plugin:name|command` IPC from JS.
24+
* Returns the selected content:// URI string, or rejects with "cancelled".
3025
*/
3126
export async function openFilePicker(): Promise<string> {
32-
const result = await invoke<{ uri: string }>('plugin:filePicker|openFile');
33-
return result.uri;
27+
return invoke<string>('pick_file_to_open');
3428
}
3529

3630
/** Response from `open_document`: native Lexical editor state + styles + metadata. */

0 commit comments

Comments
 (0)