use tauri::{plugin::TauriPlugin, Runtime};
#[cfg(target_os = "android")]
use std::sync::atomic::{AtomicBool, Ordering};
#[cfg(target_os = "android")]
use std::sync::OnceLock;
#[cfg(target_os = "android")]
use tokio::sync::broadcast;
#[cfg(target_os = "android")]
const PLUGIN_IDENTIFIER: &str = "org.jakebot.blew";
#[cfg(target_os = "android")]
static AUTO_REQUEST_PERMISSIONS: AtomicBool = AtomicBool::new(true);
#[cfg(target_os = "android")]
static PERMISSIONS_TX: OnceLock<broadcast::Sender<BlePermissionStatus>> = OnceLock::new();
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BlePermissionStatus {
Granted,
Denied,
}
impl BlePermissionStatus {
#[must_use]
pub fn is_granted(self) -> bool {
matches!(self, BlePermissionStatus::Granted)
}
}
#[derive(Clone, Debug)]
pub struct BlewPluginConfig {
pub auto_request_permissions: bool,
}
impl Default for BlewPluginConfig {
fn default() -> Self {
Self {
auto_request_permissions: true,
}
}
}
pub fn are_ble_permissions_granted() -> bool {
#[cfg(target_os = "android")]
{
blew::platform::android::are_ble_permissions_granted()
}
#[cfg(not(target_os = "android"))]
{
true
}
}
pub fn request_ble_permissions() {
#[cfg(target_os = "android")]
{
blew::platform::android::request_ble_permissions();
}
}
#[cfg(target_os = "android")]
pub fn permission_events() -> blew::util::BroadcastEventStream<BlePermissionStatus> {
let tx = PERMISSIONS_TX.get_or_init(|| broadcast::channel(16).0);
blew::util::BroadcastEventStream::new(tx.subscribe())
}
pub fn is_emulator() -> bool {
#[cfg(target_os = "android")]
{
blew::platform::android::is_emulator()
}
#[cfg(not(target_os = "android"))]
{
std::env::var("SIMULATOR_DEVICE_NAME").is_ok()
}
}
pub fn init<R: Runtime>() -> TauriPlugin<R> {
init_with_config(BlewPluginConfig::default())
}
pub fn init_with_config<R: Runtime>(config: BlewPluginConfig) -> TauriPlugin<R> {
#[cfg(target_os = "android")]
{
AUTO_REQUEST_PERMISSIONS.store(config.auto_request_permissions, Ordering::Relaxed);
let _ = PERMISSIONS_TX.get_or_init(|| broadcast::channel(16).0);
}
#[cfg(not(target_os = "android"))]
{
let _ = config;
}
tauri::plugin::Builder::<R>::new("blew")
.setup(|_app, api| {
#[cfg(target_os = "android")]
{
let vm_ptr = install_android_context()?;
let vm = unsafe { jni::JavaVM::from_raw(vm_ptr.cast()) };
blew::platform::android::init_jvm(vm);
api.register_android_plugin(PLUGIN_IDENTIFIER, "BlewPlugin")?;
}
let _ = api;
Ok(())
})
.build()
}
#[cfg(target_os = "android")]
fn install_android_context() -> Result<*mut std::ffi::c_void, Box<dyn std::error::Error>> {
use std::ffi::c_void;
use std::sync::mpsc;
use tauri::wry::prelude::{dispatch, jni as wry_jni};
let (tx, rx) = mpsc::channel();
dispatch(move |env, activity, _webview| {
let result: Result<_, wry_jni::errors::Error> = (|| {
let vm = env.get_java_vm()?;
let activity_global = env.new_global_ref(activity)?;
Ok((vm, activity_global))
})();
let _ = tx.send(result);
});
let (vm, activity_global) = rx
.recv()
.map_err(|e| format!("wry JNI dispatch never returned: {e}"))?
.map_err(|e| format!("JNI error capturing JVM/activity: {e}"))?;
let vm_ptr = vm.get_java_vm_pointer() as *mut c_void;
let activity_ptr = activity_global.as_obj().as_raw() as *mut c_void;
unsafe {
ndk_context::initialize_android_context(vm_ptr, activity_ptr);
}
std::mem::forget(activity_global);
Ok(vm_ptr)
}
#[cfg(target_os = "android")]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn Java_org_jakebot_blew_BlewPluginNative_autoRequestPermissionsEnabled(
_env: jni::EnvUnowned,
_class: jni::objects::JClass,
) -> jni::sys::jboolean {
AUTO_REQUEST_PERMISSIONS.load(Ordering::Relaxed)
}
#[cfg(target_os = "android")]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn Java_org_jakebot_blew_BlewPluginNative_onPermissionsChanged(
_env: jni::EnvUnowned,
_class: jni::objects::JClass,
granted: jni::sys::jboolean,
) {
let status = if granted {
BlePermissionStatus::Granted
} else {
BlePermissionStatus::Denied
};
if let Some(tx) = PERMISSIONS_TX.get() {
let _ = tx.send(status);
}
}