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 ctx = ndk_context::android_context();
140                let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().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/// JNI entry point invoked from `BlewPluginNative.autoRequestPermissionsEnabled()`
151/// on Android to read the flag set by [`init_with_config`].
152///
153/// # Safety
154///
155/// Invoked by the JVM through the normal JNI calling convention; safe provided
156/// the signature matches the Kotlin `external fun` declaration.
157#[cfg(target_os = "android")]
158#[unsafe(no_mangle)]
159pub unsafe extern "C" fn Java_org_jakebot_blew_BlewPluginNative_autoRequestPermissionsEnabled(
160    _env: jni::EnvUnowned,
161    _class: jni::objects::JClass,
162) -> jni::sys::jboolean {
163    AUTO_REQUEST_PERMISSIONS.load(Ordering::Relaxed)
164}
165
166/// JNI entry point invoked from `BlewPluginNative.onPermissionsChanged(granted)`
167/// on Android when the plugin's `onResume` detects a change in the BLE
168/// runtime-permission state.
169///
170/// # Safety
171///
172/// Invoked by the JVM through the normal JNI calling convention; safe provided
173/// the signature matches the Kotlin `external fun` declaration.
174#[cfg(target_os = "android")]
175#[unsafe(no_mangle)]
176pub unsafe extern "C" fn Java_org_jakebot_blew_BlewPluginNative_onPermissionsChanged(
177    _env: jni::EnvUnowned,
178    _class: jni::objects::JClass,
179    granted: jni::sys::jboolean,
180) {
181    let status = if granted {
182        BlePermissionStatus::Granted
183    } else {
184        BlePermissionStatus::Denied
185    };
186    if let Some(tx) = PERMISSIONS_TX.get() {
187        let _ = tx.send(status);
188    }
189}