Skip to main content

tauri_plugin_blew/
lib.rs

1use tauri::{plugin::TauriPlugin, Runtime};
2
3#[cfg(target_os = "android")]
4use std::sync::atomic::{AtomicBool, Ordering};
5#[cfg(target_os = "android")]
6use std::sync::OnceLock;
7
8#[cfg(target_os = "android")]
9use tokio::sync::broadcast;
10
11#[cfg(target_os = "android")]
12const PLUGIN_IDENTIFIER: &str = "org.jakebot.blew";
13
14#[cfg(target_os = "android")]
15static AUTO_REQUEST_PERMISSIONS: AtomicBool = AtomicBool::new(true);
16
17#[cfg(target_os = "android")]
18static PERMISSIONS_TX: OnceLock<broadcast::Sender<BlePermissionStatus>> = OnceLock::new();
19
20/// Current status of the aggregate Android BLE runtime permissions.
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub enum BlePermissionStatus {
23    /// All BLE runtime permissions required for the current Android API level
24    /// have been granted.
25    Granted,
26    /// At least one required BLE runtime permission is not granted.
27    Denied,
28}
29
30impl BlePermissionStatus {
31    #[must_use]
32    pub fn is_granted(self) -> bool {
33        matches!(self, BlePermissionStatus::Granted)
34    }
35}
36
37/// Plugin configuration.
38#[derive(Clone, Debug)]
39pub struct BlewPluginConfig {
40    /// If `true` (default), Android BLE runtime permissions are requested
41    /// immediately when the plugin loads. Set to `false` to defer the request
42    /// so your app can show an explanation modal first, then call
43    /// [`request_ble_permissions`] when the user is ready.
44    pub auto_request_permissions: bool,
45}
46
47impl Default for BlewPluginConfig {
48    fn default() -> Self {
49        Self {
50            auto_request_permissions: true,
51        }
52    }
53}
54
55/// Check whether Android BLE runtime permissions have been granted.
56///
57/// Always returns `true` on non-Android platforms.
58pub fn are_ble_permissions_granted() -> bool {
59    #[cfg(target_os = "android")]
60    {
61        blew::platform::android::are_ble_permissions_granted()
62    }
63    #[cfg(not(target_os = "android"))]
64    {
65        true
66    }
67}
68
69/// Trigger the Android BLE runtime permissions dialog.
70///
71/// Fire-and-forget — the dialog is presented asynchronously on the host
72/// activity's UI thread. Use [`permission_events`] to observe when the user
73/// responds (or check [`are_ble_permissions_granted`]).
74///
75/// Must be called after the Tauri app has finished initializing the plugin
76/// (the Android side needs the host `Activity`, which is captured during
77/// plugin load). No-op on non-Android platforms.
78pub fn request_ble_permissions() {
79    #[cfg(target_os = "android")]
80    {
81        blew::platform::android::request_ble_permissions();
82    }
83}
84
85/// Subscribe to Android BLE permission-change events.
86///
87/// Emits a [`BlePermissionStatus`] whenever the aggregate BLE-permission state
88/// flips between granted and denied. Changes are detected on each activity
89/// resume, so this catches both in-app dialog responses and out-of-app toggles
90/// (e.g. the user flipping a switch in system Settings while the app is
91/// backgrounded).
92///
93/// On non-Android platforms this returns an empty stream — the pattern is
94/// iOS-specific on Apple (use the existing Central/Peripheral state streams,
95/// which surface `CBManager` authorization changes via `centralManagerDidUpdateState:`).
96#[cfg(target_os = "android")]
97pub fn permission_events() -> blew::util::BroadcastEventStream<BlePermissionStatus> {
98    let tx = PERMISSIONS_TX.get_or_init(|| broadcast::channel(16).0);
99    blew::util::BroadcastEventStream::new(tx.subscribe())
100}
101
102/// Check whether the app is running on an emulator or simulator.
103///
104/// Returns `true` on Android emulators and iOS simulators, `false` on real devices
105/// and non-mobile platforms.
106pub fn is_emulator() -> bool {
107    #[cfg(target_os = "android")]
108    {
109        blew::platform::android::is_emulator()
110    }
111    #[cfg(not(target_os = "android"))]
112    {
113        std::env::var("SIMULATOR_DEVICE_NAME").is_ok()
114    }
115}
116
117/// Initialize the plugin with default configuration (auto-requests BLE
118/// permissions on load).
119pub fn init<R: Runtime>() -> TauriPlugin<R> {
120    init_with_config(BlewPluginConfig::default())
121}
122
123/// Initialize the plugin with a custom [`BlewPluginConfig`].
124pub fn init_with_config<R: Runtime>(config: BlewPluginConfig) -> TauriPlugin<R> {
125    #[cfg(target_os = "android")]
126    {
127        AUTO_REQUEST_PERMISSIONS.store(config.auto_request_permissions, Ordering::Relaxed);
128        let _ = PERMISSIONS_TX.get_or_init(|| broadcast::channel(16).0);
129    }
130    #[cfg(not(target_os = "android"))]
131    {
132        let _ = config;
133    }
134
135    tauri::plugin::Builder::<R>::new("blew")
136        .setup(|_app, api| {
137            #[cfg(target_os = "android")]
138            {
139                let vm_ptr = install_android_context()?;
140                let vm = unsafe { jni::JavaVM::from_raw(vm_ptr.cast()) };
141                blew::platform::android::init_jvm(vm);
142                api.register_android_plugin(PLUGIN_IDENTIFIER, "BlewPlugin")?;
143            }
144            let _ = api;
145            Ok(())
146        })
147        .build()
148}
149
150// Tauri 2.11 (tao 0.35) no longer calls `ndk_context::initialize_android_context`,
151// so we install it ourselves before init_jvm runs — both blew and `hickory-resolver`
152// (iroh's DNS dep on Android) read the JVM/activity through ndk_context.
153#[cfg(target_os = "android")]
154fn install_android_context() -> Result<*mut std::ffi::c_void, Box<dyn std::error::Error>> {
155    use std::ffi::c_void;
156    use std::sync::mpsc;
157    use tauri::wry::prelude::{dispatch, jni as wry_jni};
158
159    let (tx, rx) = mpsc::channel();
160    dispatch(move |env, activity, _webview| {
161        let result: Result<_, wry_jni::errors::Error> = (|| {
162            let vm = env.get_java_vm()?;
163            let activity_global = env.new_global_ref(activity)?;
164            Ok((vm, activity_global))
165        })();
166        let _ = tx.send(result);
167    });
168    let (vm, activity_global) = rx
169        .recv()
170        .map_err(|e| format!("wry JNI dispatch never returned: {e}"))?
171        .map_err(|e| format!("JNI error capturing JVM/activity: {e}"))?;
172
173    let vm_ptr = vm.get_java_vm_pointer() as *mut c_void;
174    let activity_ptr = activity_global.as_obj().as_raw() as *mut c_void;
175    unsafe {
176        ndk_context::initialize_android_context(vm_ptr, activity_ptr);
177    }
178    // ndk_context borrows the activity for the lifetime of the process; leak the
179    // global ref so the JNI ref the activity pointer refers to is never freed.
180    std::mem::forget(activity_global);
181    Ok(vm_ptr)
182}
183
184/// JNI entry point invoked from `BlewPluginNative.autoRequestPermissionsEnabled()`
185/// on Android to read the flag set by [`init_with_config`].
186///
187/// # Safety
188///
189/// Invoked by the JVM through the normal JNI calling convention; safe provided
190/// the signature matches the Kotlin `external fun` declaration.
191#[cfg(target_os = "android")]
192#[unsafe(no_mangle)]
193pub unsafe extern "C" fn Java_org_jakebot_blew_BlewPluginNative_autoRequestPermissionsEnabled(
194    _env: jni::EnvUnowned,
195    _class: jni::objects::JClass,
196) -> jni::sys::jboolean {
197    AUTO_REQUEST_PERMISSIONS.load(Ordering::Relaxed)
198}
199
200/// JNI entry point invoked from `BlewPluginNative.onPermissionsChanged(granted)`
201/// on Android when the plugin's `onResume` detects a change in the BLE
202/// runtime-permission state.
203///
204/// # Safety
205///
206/// Invoked by the JVM through the normal JNI calling convention; safe provided
207/// the signature matches the Kotlin `external fun` declaration.
208#[cfg(target_os = "android")]
209#[unsafe(no_mangle)]
210pub unsafe extern "C" fn Java_org_jakebot_blew_BlewPluginNative_onPermissionsChanged(
211    _env: jni::EnvUnowned,
212    _class: jni::objects::JClass,
213    granted: jni::sys::jboolean,
214) {
215    let status = if granted {
216        BlePermissionStatus::Granted
217    } else {
218        BlePermissionStatus::Denied
219    };
220    if let Some(tx) = PERMISSIONS_TX.get() {
221        let _ = tx.send(status);
222    }
223}