feat(desktop): split main.rs into lib.rs for Tauri Mobile (Android/iOS)
Tauri 2.x Mobile links the app as a cdylib loaded from a Java Activity, so all of the Builder/command code has to live in a library crate. Move the existing logic verbatim into src/lib.rs::run() and reduce src/main.rs to a two-line desktop entry point that calls into it. Cargo.toml gets a [lib] section (crate-types: staticlib + cdylib + rlib, named wzp_desktop_lib) and the wzp-client dependency — which pulls CPAL + VoiceProcessingIO — is moved behind cfg(not(target_os = "android")) so the Android cdylib doesn't need an audio backend yet. Engine-backed Tauri commands (connect/disconnect/toggle_mic/toggle_speaker/get_status) get Android stubs that return clear "not yet wired" errors. The signaling commands (register_signal/place_call/answer_call/get_signal_status/ ping_relay/get_identity) are platform-independent and unchanged. Also: get_identity / register_signal now auto-create the seed if missing instead of erroring with "connect to a room first", and the identity dir resolves to /data/data/com.wzp.phone/files/.wzp on Android (proper app-internal storage) vs \$HOME/.wzp on desktop. Side note: src/main.rs was previously untracked — desktop builds were working only because it existed in the local worktree. This commit fixes that too. Step 1 of the Android rewrite plan (tauri-mobile scaffold). No audio yet. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,16 @@ edition = "2024"
|
|||||||
description = "WarzonePhone Desktop — encrypted VoIP client"
|
description = "WarzonePhone Desktop — encrypted VoIP client"
|
||||||
default-run = "wzp-desktop"
|
default-run = "wzp-desktop"
|
||||||
|
|
||||||
|
# Library target — required for Tauri mobile (Android/iOS link the app as a cdylib)
|
||||||
|
# and also used by the desktop binary below.
|
||||||
|
[lib]
|
||||||
|
name = "wzp_desktop_lib"
|
||||||
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "wzp-desktop"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "2", features = [] }
|
tauri-build = { version = "2", features = [] }
|
||||||
|
|
||||||
@@ -19,12 +29,16 @@ tracing-subscriber = "0.3"
|
|||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
rustls = { version = "0.23", default-features = false, features = ["ring", "std"] }
|
rustls = { version = "0.23", default-features = false, features = ["ring", "std"] }
|
||||||
|
|
||||||
# WarzonePhone crates
|
# WarzonePhone crates — protocol layer is platform-independent
|
||||||
wzp-proto = { path = "../../crates/wzp-proto" }
|
wzp-proto = { path = "../../crates/wzp-proto" }
|
||||||
wzp-codec = { path = "../../crates/wzp-codec" }
|
wzp-codec = { path = "../../crates/wzp-codec" }
|
||||||
wzp-fec = { path = "../../crates/wzp-fec" }
|
wzp-fec = { path = "../../crates/wzp-fec" }
|
||||||
wzp-crypto = { path = "../../crates/wzp-crypto" }
|
wzp-crypto = { path = "../../crates/wzp-crypto" }
|
||||||
wzp-transport = { path = "../../crates/wzp-transport" }
|
wzp-transport = { path = "../../crates/wzp-transport" }
|
||||||
|
|
||||||
|
# wzp-client pulls CPAL + (on macOS) VoiceProcessingIO — desktop only.
|
||||||
|
# Android gets an oboe/AAudio backend in Step 3 of the Tauri mobile rewrite.
|
||||||
|
[target.'cfg(not(target_os = "android"))'.dependencies]
|
||||||
wzp-client = { path = "../../crates/wzp-client", features = ["audio", "vpio"] }
|
wzp-client = { path = "../../crates/wzp-client", features = ["audio", "vpio"] }
|
||||||
|
|
||||||
# Platform-specific
|
# Platform-specific
|
||||||
|
|||||||
465
desktop/src-tauri/src/lib.rs
Normal file
465
desktop/src-tauri/src/lib.rs
Normal file
@@ -0,0 +1,465 @@
|
|||||||
|
// WarzonePhone Tauri backend — shared between desktop (macOS/Windows/Linux)
|
||||||
|
// and Tauri mobile (Android/iOS). Platform-specific audio is cfg-gated.
|
||||||
|
|
||||||
|
#![cfg_attr(
|
||||||
|
all(not(debug_assertions), target_os = "windows"),
|
||||||
|
windows_subsystem = "windows"
|
||||||
|
)]
|
||||||
|
|
||||||
|
// CPAL-backed audio engine — desktop only. On Android we'll plug in an
|
||||||
|
// oboe/AAudio backend in a later step.
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
mod engine;
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
use engine::CallEngine;
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tauri::Emitter;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
use wzp_proto::MediaTransport;
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize)]
|
||||||
|
struct CallEvent {
|
||||||
|
kind: String,
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize)]
|
||||||
|
struct Participant {
|
||||||
|
fingerprint: String,
|
||||||
|
alias: Option<String>,
|
||||||
|
relay_label: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize)]
|
||||||
|
struct CallStatus {
|
||||||
|
active: bool,
|
||||||
|
mic_muted: bool,
|
||||||
|
spk_muted: bool,
|
||||||
|
participants: Vec<Participant>,
|
||||||
|
encode_fps: u64,
|
||||||
|
recv_fps: u64,
|
||||||
|
audio_level: u32,
|
||||||
|
call_duration_secs: f64,
|
||||||
|
fingerprint: String,
|
||||||
|
tx_codec: String,
|
||||||
|
rx_codec: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AppState {
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
engine: Mutex<Option<CallEngine>>,
|
||||||
|
signal: Arc<Mutex<SignalState>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ping result with RTT and server identity hash.
|
||||||
|
#[derive(Clone, Serialize)]
|
||||||
|
struct PingResult {
|
||||||
|
rtt_ms: u32,
|
||||||
|
/// Server identity: SHA-256 of the QUIC peer certificate, hex-encoded.
|
||||||
|
server_fingerprint: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ping a relay to check if it's online, measure RTT, and get server identity.
|
||||||
|
#[tauri::command]
|
||||||
|
async fn ping_relay(relay: String) -> Result<PingResult, String> {
|
||||||
|
let addr: std::net::SocketAddr = relay.parse().map_err(|e| format!("bad address: {e}"))?;
|
||||||
|
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||||
|
let bind: std::net::SocketAddr = "0.0.0.0:0".parse().unwrap();
|
||||||
|
let endpoint = wzp_transport::create_endpoint(bind, None).map_err(|e| format!("{e}"))?;
|
||||||
|
let client_cfg = wzp_transport::client_config();
|
||||||
|
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
let conn_result = tokio::time::timeout(
|
||||||
|
std::time::Duration::from_secs(3),
|
||||||
|
wzp_transport::connect(&endpoint, addr, "ping", client_cfg),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Always close endpoint to prevent resource leaks
|
||||||
|
endpoint.close(0u32.into(), b"done");
|
||||||
|
|
||||||
|
match conn_result {
|
||||||
|
Ok(Ok(conn)) => {
|
||||||
|
let rtt_ms = start.elapsed().as_millis() as u32;
|
||||||
|
|
||||||
|
let server_fingerprint = conn
|
||||||
|
.peer_identity()
|
||||||
|
.and_then(|id| id.downcast::<Vec<rustls::pki_types::CertificateDer>>().ok())
|
||||||
|
.and_then(|certs| certs.first().map(|c| {
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||||
|
c.as_ref().hash(&mut hasher);
|
||||||
|
let h = hasher.finish();
|
||||||
|
format!("{h:016x}")
|
||||||
|
}))
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
format!("{:x}", addr.ip().to_string().len() as u64 * 0x9e3779b97f4a7c15 + addr.port() as u64)
|
||||||
|
});
|
||||||
|
|
||||||
|
conn.close(0u32.into(), b"ping");
|
||||||
|
Ok(PingResult { rtt_ms, server_fingerprint })
|
||||||
|
}
|
||||||
|
Ok(Err(e)) => Err(format!("{e}")),
|
||||||
|
Err(_) => Err("timeout (3s)".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the directory where identity/config should live.
|
||||||
|
///
|
||||||
|
/// Desktop: `$HOME/.wzp`
|
||||||
|
/// Android: `/data/data/com.wzp.phone/files/.wzp` (app-internal storage)
|
||||||
|
fn identity_dir() -> std::path::PathBuf {
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
{
|
||||||
|
// Android app-internal storage. The package id must match tauri.conf.json.
|
||||||
|
return std::path::PathBuf::from("/data/data/com.wzp.phone/files/.wzp");
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
{
|
||||||
|
let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
|
||||||
|
std::path::PathBuf::from(home).join(".wzp")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn identity_path() -> std::path::PathBuf {
|
||||||
|
identity_dir().join("identity")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load the persisted seed, or generate-and-persist a new one if missing.
|
||||||
|
fn load_or_create_seed() -> Result<wzp_crypto::Seed, String> {
|
||||||
|
let path = identity_path();
|
||||||
|
if path.exists() {
|
||||||
|
let hex = std::fs::read_to_string(&path).map_err(|e| format!("read identity: {e}"))?;
|
||||||
|
return wzp_crypto::Seed::from_hex(hex.trim()).map_err(|e| format!("{e}"));
|
||||||
|
}
|
||||||
|
let seed = wzp_crypto::Seed::generate();
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
std::fs::create_dir_all(parent).map_err(|e| format!("create identity dir: {e}"))?;
|
||||||
|
}
|
||||||
|
let hex: String = seed.0.iter().map(|b| format!("{b:02x}")).collect();
|
||||||
|
std::fs::write(&path, hex).map_err(|e| format!("write identity: {e}"))?;
|
||||||
|
Ok(seed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read fingerprint, generating a fresh identity if none exists yet.
|
||||||
|
#[tauri::command]
|
||||||
|
fn get_identity() -> Result<String, String> {
|
||||||
|
let seed = load_or_create_seed()?;
|
||||||
|
Ok(seed.derive_identity().public_identity().fingerprint.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
#[tauri::command]
|
||||||
|
async fn connect(
|
||||||
|
state: tauri::State<'_, Arc<AppState>>,
|
||||||
|
app: tauri::AppHandle,
|
||||||
|
relay: String,
|
||||||
|
room: String,
|
||||||
|
alias: String,
|
||||||
|
os_aec: bool,
|
||||||
|
quality: String,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let mut engine_lock = state.engine.lock().await;
|
||||||
|
if engine_lock.is_some() {
|
||||||
|
return Err("already connected".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let app_clone = app.clone();
|
||||||
|
match CallEngine::start(relay, room, alias, os_aec, quality, move |event_kind, message| {
|
||||||
|
let _ = app_clone.emit(
|
||||||
|
"call-event",
|
||||||
|
CallEvent {
|
||||||
|
kind: event_kind.to_string(),
|
||||||
|
message: message.to_string(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(eng) => {
|
||||||
|
*engine_lock = Some(eng);
|
||||||
|
Ok("connected".into())
|
||||||
|
}
|
||||||
|
Err(e) => Err(format!("{e}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
#[tauri::command]
|
||||||
|
async fn disconnect(state: tauri::State<'_, Arc<AppState>>) -> Result<String, String> {
|
||||||
|
let mut engine_lock = state.engine.lock().await;
|
||||||
|
if let Some(engine) = engine_lock.take() {
|
||||||
|
engine.stop().await;
|
||||||
|
Ok("disconnected".into())
|
||||||
|
} else {
|
||||||
|
Err("not connected".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
#[tauri::command]
|
||||||
|
async fn toggle_mic(state: tauri::State<'_, Arc<AppState>>) -> Result<bool, String> {
|
||||||
|
let engine_lock = state.engine.lock().await;
|
||||||
|
if let Some(ref engine) = *engine_lock {
|
||||||
|
Ok(engine.toggle_mic())
|
||||||
|
} else {
|
||||||
|
Err("not connected".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
#[tauri::command]
|
||||||
|
async fn toggle_speaker(state: tauri::State<'_, Arc<AppState>>) -> Result<bool, String> {
|
||||||
|
let engine_lock = state.engine.lock().await;
|
||||||
|
if let Some(ref engine) = *engine_lock {
|
||||||
|
Ok(engine.toggle_speaker())
|
||||||
|
} else {
|
||||||
|
Err("not connected".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
#[tauri::command]
|
||||||
|
async fn get_status(state: tauri::State<'_, Arc<AppState>>) -> Result<CallStatus, String> {
|
||||||
|
let engine_lock = state.engine.lock().await;
|
||||||
|
if let Some(ref engine) = *engine_lock {
|
||||||
|
let status = engine.status().await;
|
||||||
|
Ok(CallStatus {
|
||||||
|
active: true,
|
||||||
|
mic_muted: status.mic_muted,
|
||||||
|
spk_muted: status.spk_muted,
|
||||||
|
participants: status
|
||||||
|
.participants
|
||||||
|
.into_iter()
|
||||||
|
.map(|p| Participant {
|
||||||
|
fingerprint: p.fingerprint,
|
||||||
|
alias: p.alias,
|
||||||
|
relay_label: p.relay_label,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
encode_fps: status.frames_sent,
|
||||||
|
recv_fps: status.frames_received,
|
||||||
|
audio_level: status.audio_level,
|
||||||
|
call_duration_secs: status.call_duration_secs,
|
||||||
|
fingerprint: status.fingerprint,
|
||||||
|
tx_codec: status.tx_codec,
|
||||||
|
rx_codec: status.rx_codec,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Ok(CallStatus {
|
||||||
|
active: false,
|
||||||
|
mic_muted: false,
|
||||||
|
spk_muted: false,
|
||||||
|
participants: vec![],
|
||||||
|
encode_fps: 0,
|
||||||
|
recv_fps: 0,
|
||||||
|
audio_level: 0,
|
||||||
|
call_duration_secs: 0.0,
|
||||||
|
fingerprint: String::new(),
|
||||||
|
tx_codec: String::new(),
|
||||||
|
rx_codec: String::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Android stubs for engine-backed commands ────────────────────────────────
|
||||||
|
//
|
||||||
|
// Step 1 of the Android rewrite: signal-only. Audio is wired up in Step 3.
|
||||||
|
// These keep the JS frontend happy (same `invoke` surface) without pulling
|
||||||
|
// in CPAL, which doesn't support Android.
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
#[tauri::command]
|
||||||
|
async fn connect(
|
||||||
|
_state: tauri::State<'_, Arc<AppState>>,
|
||||||
|
_app: tauri::AppHandle,
|
||||||
|
_relay: String,
|
||||||
|
_room: String,
|
||||||
|
_alias: String,
|
||||||
|
_os_aec: bool,
|
||||||
|
_quality: String,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
Err("audio backend not yet wired on Android (step 3)".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
#[tauri::command]
|
||||||
|
async fn disconnect(_state: tauri::State<'_, Arc<AppState>>) -> Result<String, String> {
|
||||||
|
Ok("not connected".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
#[tauri::command]
|
||||||
|
async fn toggle_mic(_state: tauri::State<'_, Arc<AppState>>) -> Result<bool, String> {
|
||||||
|
Err("not connected".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
#[tauri::command]
|
||||||
|
async fn toggle_speaker(_state: tauri::State<'_, Arc<AppState>>) -> Result<bool, String> {
|
||||||
|
Err("not connected".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
#[tauri::command]
|
||||||
|
async fn get_status(_state: tauri::State<'_, Arc<AppState>>) -> Result<CallStatus, String> {
|
||||||
|
Ok(CallStatus {
|
||||||
|
active: false,
|
||||||
|
mic_muted: false,
|
||||||
|
spk_muted: false,
|
||||||
|
participants: vec![],
|
||||||
|
encode_fps: 0,
|
||||||
|
recv_fps: 0,
|
||||||
|
audio_level: 0,
|
||||||
|
call_duration_secs: 0.0,
|
||||||
|
fingerprint: String::new(),
|
||||||
|
tx_codec: String::new(),
|
||||||
|
rx_codec: String::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Signaling commands — platform independent ───────────────────────────────
|
||||||
|
|
||||||
|
struct SignalState {
|
||||||
|
transport: Option<Arc<wzp_transport::QuinnTransport>>,
|
||||||
|
fingerprint: String,
|
||||||
|
signal_status: String,
|
||||||
|
incoming_call_id: Option<String>,
|
||||||
|
incoming_caller_fp: Option<String>,
|
||||||
|
incoming_caller_alias: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn register_signal(
|
||||||
|
state: tauri::State<'_, Arc<AppState>>,
|
||||||
|
app: tauri::AppHandle,
|
||||||
|
relay: String,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
use wzp_proto::SignalMessage;
|
||||||
|
|
||||||
|
let addr: std::net::SocketAddr = relay.parse().map_err(|e| format!("bad address: {e}"))?;
|
||||||
|
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||||
|
|
||||||
|
// Load or create seed automatically — no need to "connect to a room first"
|
||||||
|
let seed = load_or_create_seed()?;
|
||||||
|
let pub_id = seed.derive_identity().public_identity();
|
||||||
|
let fp = pub_id.fingerprint.to_string();
|
||||||
|
let identity_pub = *pub_id.signing.as_bytes();
|
||||||
|
|
||||||
|
let bind: std::net::SocketAddr = "0.0.0.0:0".parse().unwrap();
|
||||||
|
let endpoint = wzp_transport::create_endpoint(bind, None).map_err(|e| format!("{e}"))?;
|
||||||
|
let conn = wzp_transport::connect(&endpoint, addr, "_signal", wzp_transport::client_config())
|
||||||
|
.await.map_err(|e| format!("{e}"))?;
|
||||||
|
let transport = Arc::new(wzp_transport::QuinnTransport::new(conn));
|
||||||
|
|
||||||
|
transport.send_signal(&SignalMessage::RegisterPresence {
|
||||||
|
identity_pub, signature: vec![], alias: None,
|
||||||
|
}).await.map_err(|e| format!("{e}"))?;
|
||||||
|
|
||||||
|
match transport.recv_signal().await.map_err(|e| format!("{e}"))? {
|
||||||
|
Some(SignalMessage::RegisterPresenceAck { success: true, .. }) => {}
|
||||||
|
_ => return Err("registration failed".into()),
|
||||||
|
}
|
||||||
|
|
||||||
|
{ let mut sig = state.signal.lock().await; sig.transport = Some(transport.clone()); sig.fingerprint = fp.clone(); sig.signal_status = "registered".into(); }
|
||||||
|
|
||||||
|
let signal_state = Arc::clone(&state.signal);
|
||||||
|
let app_clone = app.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
match transport.recv_signal().await {
|
||||||
|
Ok(Some(SignalMessage::CallRinging { call_id })) => {
|
||||||
|
let mut sig = signal_state.lock().await; sig.signal_status = "ringing".into();
|
||||||
|
let _ = app_clone.emit("signal-event", serde_json::json!({"type":"ringing","call_id":call_id}));
|
||||||
|
}
|
||||||
|
Ok(Some(SignalMessage::DirectCallOffer { caller_fingerprint, caller_alias, call_id, .. })) => {
|
||||||
|
let mut sig = signal_state.lock().await; sig.signal_status = "incoming".into();
|
||||||
|
sig.incoming_call_id = Some(call_id.clone()); sig.incoming_caller_fp = Some(caller_fingerprint.clone()); sig.incoming_caller_alias = caller_alias.clone();
|
||||||
|
let _ = app_clone.emit("signal-event", serde_json::json!({"type":"incoming","call_id":call_id,"caller_fp":caller_fingerprint,"caller_alias":caller_alias}));
|
||||||
|
}
|
||||||
|
Ok(Some(SignalMessage::CallSetup { call_id, room, relay_addr })) => {
|
||||||
|
let mut sig = signal_state.lock().await; sig.signal_status = "setup".into();
|
||||||
|
let _ = app_clone.emit("signal-event", serde_json::json!({"type":"setup","call_id":call_id,"room":room,"relay_addr":relay_addr}));
|
||||||
|
}
|
||||||
|
Ok(Some(SignalMessage::Hangup { .. })) => {
|
||||||
|
let mut sig = signal_state.lock().await; sig.signal_status = "registered".into(); sig.incoming_call_id = None;
|
||||||
|
let _ = app_clone.emit("signal-event", serde_json::json!({"type":"hangup"}));
|
||||||
|
}
|
||||||
|
Ok(None) | Err(_) => break,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut sig = signal_state.lock().await; sig.signal_status = "idle".into(); sig.transport = None;
|
||||||
|
});
|
||||||
|
Ok(fp)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn place_call(state: tauri::State<'_, Arc<AppState>>, target_fp: String) -> Result<(), String> {
|
||||||
|
use wzp_proto::SignalMessage;
|
||||||
|
let sig = state.signal.lock().await;
|
||||||
|
let transport = sig.transport.as_ref().ok_or("not registered")?;
|
||||||
|
let call_id = format!("{:016x}", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos());
|
||||||
|
transport.send_signal(&SignalMessage::DirectCallOffer {
|
||||||
|
caller_fingerprint: sig.fingerprint.clone(), caller_alias: None, target_fingerprint: target_fp,
|
||||||
|
call_id, identity_pub: [0u8; 32], ephemeral_pub: [0u8; 32], signature: vec![],
|
||||||
|
supported_profiles: vec![wzp_proto::QualityProfile::GOOD],
|
||||||
|
}).await.map_err(|e| format!("{e}"))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn answer_call(state: tauri::State<'_, Arc<AppState>>, call_id: String, mode: i32) -> Result<(), String> {
|
||||||
|
use wzp_proto::SignalMessage;
|
||||||
|
let sig = state.signal.lock().await;
|
||||||
|
let transport = sig.transport.as_ref().ok_or("not registered")?;
|
||||||
|
let accept_mode = match mode { 0 => wzp_proto::CallAcceptMode::Reject, 1 => wzp_proto::CallAcceptMode::AcceptTrusted, _ => wzp_proto::CallAcceptMode::AcceptGeneric };
|
||||||
|
transport.send_signal(&SignalMessage::DirectCallAnswer {
|
||||||
|
call_id, accept_mode, identity_pub: None, ephemeral_pub: None, signature: None,
|
||||||
|
chosen_profile: Some(wzp_proto::QualityProfile::GOOD),
|
||||||
|
}).await.map_err(|e| format!("{e}"))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn get_signal_status(state: tauri::State<'_, Arc<AppState>>) -> Result<serde_json::Value, String> {
|
||||||
|
let sig = state.signal.lock().await;
|
||||||
|
Ok(serde_json::json!({"status":sig.signal_status,"fingerprint":sig.fingerprint,"incoming_call_id":sig.incoming_call_id,"incoming_caller_fp":sig.incoming_caller_fp}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── App entry point ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Shared Tauri app builder. Used by the desktop `main.rs` and the mobile
|
||||||
|
/// entry point below.
|
||||||
|
pub fn run() {
|
||||||
|
tracing_subscriber::fmt().init();
|
||||||
|
|
||||||
|
let state = Arc::new(AppState {
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
engine: Mutex::new(None),
|
||||||
|
signal: Arc::new(Mutex::new(SignalState {
|
||||||
|
transport: None, fingerprint: String::new(), signal_status: "idle".into(),
|
||||||
|
incoming_call_id: None, incoming_caller_fp: None, incoming_caller_alias: None,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_shell::init())
|
||||||
|
.manage(state)
|
||||||
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
ping_relay, get_identity, connect, disconnect, toggle_mic, toggle_speaker, get_status,
|
||||||
|
register_signal, place_call, answer_call, get_signal_status,
|
||||||
|
])
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running WarzonePhone");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tauri mobile entry point (Android/iOS). On desktop this is a no-op —
|
||||||
|
/// `main.rs` calls `run()` directly.
|
||||||
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
|
pub fn mobile_entry() {
|
||||||
|
run();
|
||||||
|
}
|
||||||
10
desktop/src-tauri/src/main.rs
Normal file
10
desktop/src-tauri/src/main.rs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
// Desktop binary entry point. All logic lives in `lib.rs` so the same
|
||||||
|
// code can be built as a cdylib for Android/iOS via `cargo tauri android build`.
|
||||||
|
#![cfg_attr(
|
||||||
|
all(not(debug_assertions), target_os = "windows"),
|
||||||
|
windows_subsystem = "windows"
|
||||||
|
)]
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
wzp_desktop_lib::run();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user