use std::collections::HashMap;
use std::sync::Arc;
use tauri::{AppHandle, Emitter, State};
use tokio::sync::Mutex;
use hakodb::document::hako_doc::HakoDoc;
use hakodb::document::value::Value;
use hakodb::query::filter::Operator;
use hakodb::query::query::Query;
use crate::gateway::HakoGateway;
pub use hakodb::net_sync::{NetSyncer, NetworkStatus, SyncStatus};
pub const APP_STATE_COLLECTION: &str = "app_state";
pub const NET_PREFS_DOC: &str = "sync_prefs";
pub const NET_GROUP_DOC: &str = "sync_group";
pub const FALLBACK_ROOM_KEY: &str = "default";
pub const EVENT_SYNC_ON: &str = "sync_on";
pub const EVENT_SYNC_OFF: &str = "sync_off";
pub struct NetSyncState {
pub syncer: Arc<Mutex<Option<NetSyncer>>>,
pub app_handle: AppHandle,
}
impl NetSyncState {
pub fn new(app_handle: AppHandle) -> Self {
Self {
syncer: Arc::new(Mutex::new(None)),
app_handle,
}
}
}
fn read_room_key(gateway: &HakoGateway) -> Option<String> {
gateway
.db
.get(APP_STATE_COLLECTION, NET_GROUP_DOC)
.ok()
.flatten()
.and_then(|d| d.get("room_key").map(|v| v.to_json()))
.and_then(|j| j.as_str().map(|s| s.to_string()))
.filter(|k| !k.is_empty())
}
#[tauri::command]
pub async fn toggle_net_sync(
enabled: bool,
port: u16,
self_id: String,
room_key: Option<String>,
excluded: Option<Vec<String>>,
state: State<'_, NetSyncState>,
gateway: State<'_, HakoGateway>,
) -> Result<String, String> {
let mut syncer_lock = state.syncer.lock().await;
let app = state.app_handle.clone();
let current_config = gateway
.db
.get(APP_STATE_COLLECTION, NET_PREFS_DOC)
.ok()
.flatten();
let doc = current_config.unwrap_or_default();
let current_enabled = doc
.get("enabled")
.and_then(|v| v.to_json().as_bool())
.unwrap_or(false);
if enabled != current_enabled {
let mut prefs = HakoDoc::default();
prefs.insert("enabled", Value::Bool(enabled));
let _ = gateway.db.put(APP_STATE_COLLECTION, NET_PREFS_DOC, &prefs);
}
if !enabled {
if !syncer_lock.is_none() {
if let Some(s) = syncer_lock.take() {
s.stop();
}
}
let _ = app.emit(EVENT_SYNC_OFF, ());
return Ok("OFF".into());
}
if syncer_lock.is_none() {
#[cfg(target_os = "android")]
acquire_multicast_lock();
let db = Arc::clone(&gateway.db);
let room_key = room_key
.filter(|k| !k.is_empty())
.or_else(|| read_room_key(&gateway))
.unwrap_or_else(|| FALLBACK_ROOM_KEY.to_string());
let excluded = excluded.unwrap_or_else(|| vec![APP_STATE_COLLECTION.to_string()]);
let new_syncer = NetSyncer::new(db, &self_id, &room_key, excluded);
new_syncer.start(port).await.map_err(|e| e.to_string())?;
*syncer_lock = Some(new_syncer);
let _ = app.emit(EVENT_SYNC_ON, ());
}
Ok("ON".into())
}
#[tauri::command]
pub async fn get_sync_status(
state: State<'_, NetSyncState>,
) -> Result<Option<NetworkStatus>, String> {
let lock = state.syncer.lock().await;
match lock.as_ref() {
Some(s) => Ok(Some(s.status())),
None => Ok(None),
}
}
#[tauri::command]
pub async fn check_sync_security_exists(
collection: String,
gateway: State<'_, HakoGateway>,
) -> Result<bool, String> {
Ok(gateway
.db
.get(&collection, "config")
.map_err(|e| e.to_string())?
.is_some())
}
#[tauri::command]
pub async fn list_network_peers(
security_collection: String,
state: State<'_, NetSyncState>,
gateway: State<'_, HakoGateway>,
) -> Result<Vec<serde_json::Value>, String> {
let syncer_guard = state.syncer.lock().await;
let live_ids = match syncer_guard.as_ref() {
Some(s) => s.status().known_peers,
None => Vec::new(),
};
if live_ids.is_empty() {
return Ok(Vec::new());
}
let id_values: Vec<Value> = live_ids
.iter()
.map(|id| Value::String(id.clone()))
.collect();
let query =
Query::new(&security_collection).where_filter("id", Operator::In, Value::Array(id_values));
let db_results = gateway.db.query(query).map_err(|e| e.to_string())?;
let mut db_map = HashMap::new();
for (id, doc) in db_results {
db_map.insert(id, doc);
}
let mut final_list = Vec::new();
for id in live_ids {
let db_doc = db_map.get(&id);
final_list.push(serde_json::json!({
"id": id,
"is_online": true,
"status": db_doc.and_then(|d| d.get("status"))
.map(|v| v.to_json())
.unwrap_or(serde_json::json!("new_device")),
"name": db_doc.and_then(|d| d.get("name"))
.map(|v| v.to_json())
.unwrap_or(serde_json::json!("Perangkat Baru"))
}));
}
Ok(final_list)
}
#[tauri::command]
pub async fn prepare_sync_restore(
collections: Vec<String>,
gateway: State<'_, HakoGateway>,
) -> Result<Vec<String>, String> {
let mut out = Vec::with_capacity(collections.len());
for col in collections {
let purged = gateway
.db
.vacuum_collection(&col)
.map_err(|e| e.to_string())?;
gateway.db.replicate_collection(&col);
out.push(format!("{}:{}", col, purged));
}
Ok(out)
}
#[tauri::command]
pub async fn bootstrap_sync(
port: u16,
self_id: String,
excluded: Option<Vec<String>>,
state: State<'_, NetSyncState>,
gateway: State<'_, HakoGateway>,
) -> Result<(), String> {
let config = gateway
.db
.get(APP_STATE_COLLECTION, NET_PREFS_DOC)
.ok()
.flatten();
if let Some(doc) = config {
if let Some(Value::Bool(true)) = doc.get("enabled") {
let _ = toggle_net_sync(true, port, self_id, None, excluded, state, gateway).await;
}
}
Ok(())
}
#[cfg(target_os = "android")]
pub fn acquire_multicast_lock() {
use jni::objects::JObject;
let res = (|| {
let ctx = ndk_context::android_context();
let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) }.ok()?;
let mut env = vm.attach_current_thread().ok()?;
let context = unsafe { JObject::from_raw(ctx.context().cast()) };
let wifi_service_str = env.new_string("wifi").ok()?;
let wifi_manager = env.call_method(
&context,
"getSystemService",
"(Ljava/lang/String;)Ljava/lang/Object;",
&[(&wifi_service_str).into()]
).ok()?.l().ok()?;
let lock_tag = env.new_string("tokocepat_sync_lock").ok()?;
let mcast_lock = env.call_method(
&wifi_manager,
"createMulticastLock",
"(Ljava/lang/String;)Landroid/net/wifi/WifiManager$MulticastLock;",
&[(&lock_tag).into()]
).ok()?.l().ok()?;
let _ = env.call_method(&mcast_lock, "acquire", "()V", &[]);
Some(())
})();
if res.is_none() {
eprintln!("Failed to acquire Multicast Lock. Sync may not work on this WiFi.");
}
}