use std::sync::{ atomic::{AtomicBool, AtomicU64, Ordering}, Arc, Mutex, OnceLock, RwLock, }; use std::time::Duration; use base64::{ engine::general_purpose::{STANDARD, STANDARD_NO_PAD}, Engine as _, }; use mtp::client::{ClientConfig, MTPClient, MTPConnection, Policy, SendMode}; use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue, TypeMap}; use mtp::crypto::{ derive_encryption_key, AeadDecrypt, ChaCha20Poly1305, HybridKem, Keyring, PublicKeyBundle, }; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use tauri::{AppHandle, Emitter}; const EVENT_NAME: &str = "mtp://event"; const DISCONNECTED: u8 = 0; const CONNECTING: u8 = 1; const CONNECTED: u8 = 2; const CHAT_SECRET_SALT: &[u8] = b"tensamin-chat-secret-v1"; const CHAT_MESSAGE_SALT: &[u8] = b"tensamin-chat-message-v1"; const CHAT_SECRET_SCHEME: &str = "mtp-chat-secret-kem-chacha20poly1305-hkdf-sha256-v1"; #[cfg(target_os = "android")] const ROOT_YE_PEM: &[u8] = b"-----BEGIN CERTIFICATE-----\n\ MIIB2TCCAWCgAwIBAgIRAKQCa6LvbHwg1AR+XmWmk4AwCgYIKoZIzj0EAwMwLjEL\n\ MAkGA1UEBhMCVVMxDTALBgNVBAoTBElTUkcxEDAOBgNVBAMTB1Jvb3QgWUUwHhcN\n\ MjUwOTAzMDAwMDAwWhcNNDUwOTAyMjM1OTU5WjAuMQswCQYDVQQGEwJVUzENMAsG\n\ A1UEChMESVNSRzEQMA4GA1UEAxMHUm9vdCBZRTB2MBAGByqGSM49AgEGBSuBBAAi\n\ A2IABDwS/6vhrcVqcbBo+wgdI3fwn9x7DNJJOY/lTOti0vkwuRN87RhEhTH17E7X\n\ yFjWsPYhIPt/wzOqxTd2b+4ZJNy9ID04YywF9U5zasDVyGSNErVNtz8uSGh5izW8\n\ 7j77GaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O\n\ BBYEFKPIJlqOoUzQNWP8myPIOq5W809WMAoGCCqGSM49BAMDA2cAMGQCMHhMr8N9\n\ LdL1VQKs9BdV81r76eXRB6mtjuNjzk6/lBsPNToWLTDzGYgtQKO1jl63uAIwGV7m\n\ onyF377c+MM1oqVNs17sgu7F9YKZwgLmVbeOMDbKAXHtKMDLbiGllCcs8f47\n\ -----END CERTIFICATE-----\n"; #[cfg(target_os = "android")] const YE1_PEM: &[u8] = b"-----BEGIN CERTIFICATE-----\n\ MIICizCCAhGgAwIBAgIQXd1w3TH4AchcGGp6BLgK/jAKBggqhkjOPQQDAzAuMQsw\n\ CQYDVQQGEwJVUzENMAsGA1UEChMESVNSRzEQMA4GA1UEAxMHUm9vdCBZRTAeFw0y\n\ NTA5MDMwMDAwMDBaFw0yODA5MDIyMzU5NTlaMDMxCzAJBgNVBAYTAlVTMRYwFAYD\n\ VQQKEw1MZXQncyBFbmNyeXB0MQwwCgYDVQQDEwNZRTEwdjAQBgcqhkjOPQIBBgUr\n\ gQQAIgNiAAQHZVB1/mimla2hfSurylScjPMZaOJXLz/NnAc2sylm8WDyhU9Ccp+z\n\ ASQi5vSwGGJjSGklkD9fdPR8GpyDIOIjCEfrnbt/v+ZSEPLLEGbaM6EccDbN7p9x\n\ teIm2Avf+ryjge4wgeswDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUF\n\ BwMBMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFLsgykcL/tflnPmPCSqj\n\ jDdFsbzYMB8GA1UdIwQYMBaAFKPIJlqOoUzQNWP8myPIOq5W809WMDIGCCsGAQUF\n\ BwEBBCYwJDAiBggrBgEFBQcwAoYWaHR0cDovL3llLmkubGVuY3Iub3JnLzATBgNV\n\ HSAEDDAKMAgGBmeBDAECATAnBgNVHR8EIDAeMBygGqAYhhZodHRwOi8veWUuYy5s\n\ ZW5jci5vcmcvMAoGCCqGSM49BAMDA2gAMGUCMQDgjUEahFT/h3DRakqiPZpLvPgf\n\ Zwkt6K2EOMmh1nvEzl83eMLYcod4GCl3b0J1Nn0CMBNYmEQJb4CEG5WoOe7aRn/L\n\ VKu6saHmHEynI7ysIPd8zQsK1HdmhlHKlw9Z5GpGvA==\n\ -----END CERTIFICATE-----\n"; #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] #[serde(rename_all = "camelCase")] pub struct MtpConfig { pub user_id: u64, pub keyring: String, pub omega_url: String, pub forced_omikron_url: Option, pub forced_omikron_public_key: Option, } #[derive(Clone, Debug, Default, Serialize)] #[serde(rename_all = "camelCase")] pub struct MtpSnapshot { pub generation: u64, pub ready_state: u8, pub identified: bool, pub state: Option, pub error: Option, } #[derive(Clone, Serialize)] #[serde(tag = "kind", rename_all = "camelCase")] enum MtpEvent { State { snapshot: MtpSnapshot, }, Message { generation: u64, message: Value, }, Log { level: u8, message: String, details: Option, }, } pub struct MtpManager { runtime: tokio::runtime::Runtime, config: RwLock>, connection: RwLock>>, snapshot: RwLock, generation: AtomicU64, enabled: AtomicBool, ui_visible: AtomicBool, app: RwLock>, start_lock: Mutex<()>, } static MANAGER: OnceLock = OnceLock::new(); pub fn manager() -> &'static MtpManager { MANAGER.get_or_init(|| MtpManager { runtime: tokio::runtime::Builder::new_multi_thread() .enable_all() .thread_name("tensamin-mtp") .build() .expect("failed to create MTP runtime"), config: RwLock::new(None), connection: RwLock::new(None), snapshot: RwLock::new(MtpSnapshot { ready_state: DISCONNECTED, ..MtpSnapshot::default() }), generation: AtomicU64::new(0), enabled: AtomicBool::new(false), ui_visible: AtomicBool::new(false), app: RwLock::new(None), start_lock: Mutex::new(()), }) } impl MtpManager { pub fn attach_app(&self, app: AppHandle) { *self.app.write().expect("app lock poisoned") = Some(app); } pub fn configure_and_start(&'static self, config: MtpConfig) { let _guard = self.start_lock.lock().expect("start lock poisoned"); if self.enabled.load(Ordering::SeqCst) && self.config.read().expect("config lock poisoned").as_ref() == Some(&config) { return; } *self.config.write().expect("config lock poisoned") = Some(config); self.enabled.store(true, Ordering::SeqCst); let generation = self.generation.fetch_add(1, Ordering::SeqCst) + 1; if let Some(connection) = self .connection .write() .expect("connection lock poisoned") .take() { self.runtime .spawn(async move { connection.sender.close().await }); } self.set_snapshot(MtpSnapshot { generation, ready_state: CONNECTING, identified: false, state: None, error: None, }); self.runtime.spawn(supervise(generation)); } pub fn stop(&self) { self.enabled.store(false, Ordering::SeqCst); self.generation.fetch_add(1, Ordering::SeqCst); if let Some(connection) = self .connection .write() .expect("connection lock poisoned") .take() { self.runtime .spawn(async move { connection.sender.close().await }); } self.set_snapshot(MtpSnapshot { generation: self.generation.load(Ordering::SeqCst), ready_state: DISCONNECTED, identified: false, state: None, error: None, }); } pub fn set_ui_visible(&self, visible: bool) { self.ui_visible.store(visible, Ordering::SeqCst); } pub fn is_enabled(&self) -> bool { self.enabled.load(Ordering::SeqCst) } pub fn snapshot(&self) -> MtpSnapshot { self.snapshot .read() .expect("snapshot lock poisoned") .clone() } fn set_snapshot(&self, snapshot: MtpSnapshot) { if let Some(error) = snapshot.error.as_ref() { eprintln!("android: MTP connection failed: {error}"); } *self.snapshot.write().expect("snapshot lock poisoned") = snapshot.clone(); self.emit(MtpEvent::State { snapshot }); } fn emit(&self, event: MtpEvent) { if let Some(app) = self.app.read().expect("app lock poisoned").as_ref() { let _ = app.emit(EVENT_NAME, event); } } fn log(&self, level: u8, message: impl Into, details: Option) { let message = message.into(); eprintln!( "android: {message}{}", details .as_ref() .map(|value| format!(": {value}")) .unwrap_or_default() ); self.emit(MtpEvent::Log { level, message, details, }); } fn is_current(&self, generation: u64) -> bool { self.enabled.load(Ordering::SeqCst) && self.generation.load(Ordering::SeqCst) == generation } async fn request( &self, type_name: &str, data: Value, id: Option, ) -> Result { let connection = self .connection .read() .map_err(|_| "MTP connection lock is unavailable")? .clone() .ok_or_else(|| "MTP is not connected".to_string())?; let request = json_to_frame(type_name, data, id)?; let response = connection .request(&request, None) .await .map_err(|error| error.to_string())?; frame_to_json(&response) } } async fn supervise(generation: u64) { let manager = manager(); let mut delay = Duration::from_secs(1); while manager.is_current(generation) { let config = manager.config.read().expect("config lock poisoned").clone(); let Some(config) = config else { return }; manager.log(2, "Starting native MTP connection", None); android_status("Connecting"); match connect(&config).await { Ok((connection, state)) => { delay = Duration::from_secs(1); let connection = Arc::new(connection); let stale = { let _guard = manager.start_lock.lock().expect("start lock poisoned"); let mut current = manager .connection .write() .expect("connection lock poisoned"); if !manager.is_current(generation) { true } else { *current = Some(connection.clone()); drop(current); manager.set_snapshot(MtpSnapshot { generation, ready_state: CONNECTED, identified: true, state: Some(state), error: None, }); false } }; if stale { connection.sender.close().await; break; } android_status("Connected"); manager.log(2, "Native MTP connection established", None); while manager.is_current(generation) { match connection.receive().await { Ok(frame) => handle_push(generation, connection.clone(), frame).await, Err(error) => { let _guard = manager.start_lock.lock().expect("start lock poisoned"); if manager.is_current(generation) { manager.set_snapshot(MtpSnapshot { generation, ready_state: DISCONNECTED, identified: false, state: None, error: Some(error.to_string()), }); } break; } } } let mut current = manager .connection .write() .expect("connection lock poisoned"); if current .as_ref() .is_some_and(|stored| Arc::ptr_eq(stored, &connection)) { current.take(); } } Err(error) => { let _guard = manager.start_lock.lock().expect("start lock poisoned"); if manager.is_current(generation) { manager.set_snapshot(MtpSnapshot { generation, ready_state: DISCONNECTED, identified: false, state: None, error: Some(error), }); } } } if !manager.is_current(generation) { break; } android_status("Reconnecting"); tokio::time::sleep(delay).await; delay = (delay * 2).min(Duration::from_secs(60)); } } async fn connect(config: &MtpConfig) -> Result<(MTPConnection, Value), String> { let (url, public_key) = resolve_endpoint(config) .await .map_err(|error| format!("endpoint discovery failed: {error}"))?; manager().log( 2, "Resolved Omikron endpoint", Some(Value::String(url.clone())), ); let keyring_bytes = decode_browser_base64(&config.keyring) .map_err(|error| format!("invalid MTP keyring: {error}"))?; let keyring = Keyring::from_bytes(&keyring_bytes) .map_err(|error| format!("invalid MTP keyring: {error}"))?; let host_key_bytes = decode_sdk_bytes(&public_key) .map_err(|error| format!("invalid Omikron public key: {error}"))?; let host_key = PublicKeyBundle::from_bytes(&host_key_bytes) .map_err(|error| format!("invalid Omikron public key: {error}"))?; host_key .validate() .map_err(|error| format!("invalid Omikron public key: {error}"))?; let client_config = ClientConfig::new(url) .with_client_id(config.user_id) .with_description("client") .with_policy(Policy::default().with_send_mode(SendMode::SingleStreamPerMessage)) .with_ping_interval(Duration::from_secs(30)) .with_max_missed_pings(3); #[cfg(target_os = "android")] let client_config = client_config.with_pinned_pem(android_root_certificates().clone()); let connection = MTPClient::auth_connect(client_config, &keyring, &host_key) .await .map_err(|error| format!("transport authentication failed: {error}"))?; manager().log(2, "Native MTP authentication completed", None); let connected = CommunicationValue::new(CommunicationType::ClientConnected) .add_typed_default( DataType::SessionId, DataValue::UnsignedNumber(current_millis() as u128), ) .add_typed_default(DataType::VersionNumber, DataValue::UnsignedNumber(0)) .add_typed_default(DataType::CacheValid, DataValue::BoolFalse) .add_typed_default(DataType::CacheSchemaVersion, DataValue::UnsignedNumber(0)); let state = connection .request(&connected, None) .await .map_err(|error| format!("initial state synchronization failed: {error}"))?; if !state.is_type(CommunicationType::ClientStateSync) { return Err(format!( "expected ClientStateSync, received {}", state.get_type_name().unwrap_or("unknown") )); } let session_id = state .get_data(DataType::SessionId) .as_number() .ok_or("ClientStateSync omitted SessionId")?; let version = state .get_data(DataType::VersionNumber) .as_number() .ok_or("ClientStateSync omitted VersionNumber")?; let ack = CommunicationValue::new(CommunicationType::ClientStateAck) .add_typed_default(DataType::SessionId, number_to_data(session_id)) .add_typed_default(DataType::VersionNumber, number_to_data(version)); let response = connection .request(&ack, None) .await .map_err(|error| format!("state acknowledgement failed: {error}"))?; if response .get_type_name() .is_some_and(|name| name.starts_with("Error")) { return Err(format!("ClientStateAck failed: {response}")); } Ok((connection, frame_data_to_json(&state)?)) } async fn resolve_endpoint(config: &MtpConfig) -> Result<(String, String), String> { if let (Some(url), Some(key)) = ( config.forced_omikron_url.as_ref(), config.forced_omikron_public_key.as_ref(), ) { return Ok((url.clone(), key.clone())); } #[derive(Deserialize)] struct Omikron { ip_address: String, port: u16, public_key: String, } let root = config.omega_url.trim_end_matches('/'); let response = reqwest::get(format!("{root}/api/get/omikron/{}", config.user_id)) .await .map_err(|error| error.to_string())?; if !response.status().is_success() { return Err(format!("Omikron lookup failed: {}", response.status())); } let data: Omikron = response.json().await.map_err(|error| error.to_string())?; Ok(( format!("https://{}:{}", data.ip_address, data.port), data.public_key, )) } async fn handle_push(generation: u64, connection: Arc, frame: CommunicationValue) { let manager = manager(); if let Ok(message) = frame_to_json(&frame) { manager.emit(MtpEvent::Message { generation, message, }); } if frame.is_type(CommunicationType::MessageState) && frame.get_str(DataType::MessageState) == Some("read") { if let Some(partner_id) = frame .get_data(DataType::ChatPartnerId) .as_number() .and_then(|value| u64::try_from(value).ok()) { if let Err(error) = android_cancel_notification(partner_id) { eprintln!("failed to clear read message notification: {error}"); } } } if frame.is_type(CommunicationType::MessageLive) && !manager.ui_visible.load(Ordering::SeqCst) { if let Err(error) = notify_message(connection, &frame).await { eprintln!("failed to create background message notification: {error}"); } } } async fn notify_message( connection: Arc, frame: &CommunicationValue, ) -> Result<(), String> { let sender_id = frame .get_data(DataType::SenderId) .as_number() .and_then(|value| u64::try_from(value).ok()) .ok_or("MessageLive omitted SenderId")?; let message = frame.get_data(DataType::Message); let content = container_value(message, DataType::Content) .and_then(DataValue::as_str) .ok_or("MessageLive omitted Content")?; let config = manager() .config .read() .expect("config lock poisoned") .clone() .ok_or("missing config")?; let keyring_bytes = decode_browser_base64(&config.keyring)?; let keyring = Keyring::from_bytes(&keyring_bytes).map_err(|error| error.to_string())?; let chat_id = derive_chat_id(config.user_id, sender_id); let secret_id = format!("chat:{chat_id}:main"); let secret_request = CommunicationValue::new(CommunicationType::GetChatSecret) .add_typed_default(DataType::UserId, DataValue::Str(config.user_id.to_string())) .add_typed_default(DataType::ChatId, DataValue::Str(chat_id.clone())) .add_typed_default(DataType::SecretId, DataValue::Str(secret_id.clone())); let secret = connection .request(&secret_request, None) .await .map_err(|error| error.to_string())?; if !secret.is_type(CommunicationType::ChatSecretResponse) { return Err(format!("GetChatSecret failed: {secret}")); } if secret.get_str(DataType::WrappingScheme) != Some(CHAT_SECRET_SCHEME) { return Err("unsupported chat secret wrapping scheme".into()); } let version = secret .get_data(DataType::VersionNumber) .as_number() .ok_or("missing secret version")?; let encrypted_secret = secret .get_bytes(DataType::EncryptedSecret) .ok_or("missing encrypted secret")?; let kem_ciphertext = secret .get_bytes(DataType::KemCiphertext) .ok_or("missing KEM ciphertext")?; let shared = HybridKem::decapsulate(&keyring.kem_secret_key, kem_ciphertext) .map_err(|error| error.to_string())?; let wrapping_key = derive_encryption_key( &shared, CHAT_SECRET_SALT, format!("{chat_id}:{secret_id}:{version}").as_bytes(), ) .map_err(|error| error.to_string())?; let chat_secret = ChaCha20Poly1305::new(wrapping_key) .decrypt(encrypted_secret, b"") .map_err(|error| error.to_string())?; let message_key = derive_encryption_key(&chat_secret, CHAT_MESSAGE_SALT, b"message-content") .map_err(|error| error.to_string())?; let ciphertext = STANDARD .decode(content) .map_err(|error| error.to_string())?; let plaintext = ChaCha20Poly1305::new(message_key) .decrypt(&ciphertext, b"") .map_err(|error| error.to_string())?; let user_request = CommunicationValue::new(CommunicationType::GetUserData).add_typed_default( DataType::UserId, DataValue::UnsignedNumber(sender_id as u128), ); let user = connection .request(&user_request, None) .await .map_err(|error| error.to_string())?; let sender = user .get_str(DataType::Display) .or_else(|| user.get_str(DataType::Username)) .map(str::to_owned) .unwrap_or_else(|| format!("User {sender_id}")); let avatar = user .get_str(DataType::Avatar) .and_then(|avatar| decode_browser_base64(avatar).ok()); android_notify( sender_id, &sender, &String::from_utf8_lossy(&plaintext), avatar.as_deref(), )?; Ok(()) } fn derive_chat_id(own: u64, peer: u64) -> String { format!("{}:{}", own.min(peer), own.max(peer)) } fn decode_browser_base64(value: &str) -> Result, String> { let normalized: String = value .chars() .filter(|character| !character.is_ascii_whitespace()) .collect(); if normalized.is_empty() { return Err("value is empty".into()); } STANDARD .decode(&normalized) .or_else(|_| STANDARD_NO_PAD.decode(&normalized)) .map_err(|_| "invalid base64 encoding".into()) } fn decode_sdk_bytes(value: &str) -> Result, String> { let trimmed = value.trim(); let hex: String = trimmed .strip_prefix("0x") .or_else(|| trimmed.strip_prefix("0X")) .unwrap_or(trimmed) .chars() .filter(|character| !matches!(character, ' ' | '\t' | '\r' | '\n' | ':' | '_' | '-')) .collect(); if !hex.is_empty() && hex.chars().all(|character| character.is_ascii_hexdigit()) { if hex.len() % 2 != 0 { return Err("hex string has an odd length".into()); } return (0..hex.len()) .step_by(2) .map(|offset| { u8::from_str_radix(&hex[offset..offset + 2], 16) .map_err(|_| "invalid hex encoding".to_string()) }) .collect(); } decode_browser_base64(trimmed) } #[cfg(target_os = "android")] fn android_root_certificates() -> &'static Vec { static ROOTS: OnceLock> = OnceLock::new(); ROOTS.get_or_init(|| { let mut pem = Vec::new(); for certificate in webpki_root_certs::TLS_SERVER_ROOT_CERTS { pem.extend_from_slice(b"-----BEGIN CERTIFICATE-----\n"); let encoded = STANDARD.encode(certificate.as_ref()); for line in encoded.as_bytes().chunks(64) { pem.extend_from_slice(line); pem.push(b'\n'); } pem.extend_from_slice(b"-----END CERTIFICATE-----\n"); } pem.extend_from_slice(ROOT_YE_PEM); pem.extend_from_slice(YE1_PEM); pem }) } fn container_value(value: &DataValue, field: DataType) -> Option<&DataValue> { let id = field.try_to_id(&TypeMap::latest())?; value.get_field(id) } fn json_to_frame( type_name: &str, data: Value, id: Option, ) -> Result { let comm_type = CommunicationType::from_name(type_name) .ok_or_else(|| format!("unknown communication type: {type_name}"))?; let mut frame = CommunicationValue::new(comm_type); if let Some(id) = id { frame = frame.with_id(id); } let Value::Object(fields) = data else { return Err("MTP request data must be an object".into()); }; for (name, value) in fields { let data_type = DataType::from_name(&name).ok_or_else(|| format!("unknown data type: {name}"))?; frame = frame.add_typed_default(data_type, json_to_data(&name, value)?); } Ok(frame) } fn json_to_data(field: &str, value: Value) -> Result { Ok(match value { Value::Null => DataValue::Null, Value::Bool(value) => DataValue::from(value), Value::Number(value) => { if let Some(integer) = value.as_u64() { DataValue::UnsignedNumber(integer as u128) } else if let Some(integer) = value.as_i64() { DataValue::SignedNumber(integer as i128) } else { DataValue::Float(value.as_f64().ok_or("invalid number")?) } } Value::String(value) => DataValue::Str(value), Value::Array(values) if is_bytes_field(field) => DataValue::Bytes( values .into_iter() .map(|value| { value .as_u64() .and_then(|n| u8::try_from(n).ok()) .ok_or("invalid byte") }) .collect::, _>>()?, ), Value::Array(values) => DataValue::Array( values .into_iter() .map(|value| json_to_data(field, value)) .collect::>()?, ), Value::Object(fields) => { let mut entries = Vec::with_capacity(fields.len()); for (name, value) in fields { let data_type = DataType::from_name(&name) .ok_or_else(|| format!("unknown nested data type: {name}"))?; let id = data_type .try_to_id(&TypeMap::latest()) .ok_or_else(|| format!("unmapped data type: {name}"))?; entries.push((id, json_to_data(&name, value)?)); } DataValue::Container(entries) } }) } fn number_to_data(value: i128) -> DataValue { if value >= 0 { DataValue::UnsignedNumber(value as u128) } else { DataValue::SignedNumber(value) } } fn is_bytes_field(field: &str) -> bool { matches!( field, "EncryptedSecret" | "KemCiphertext" | "Payload" | "PublicKeys" ) } fn frame_to_json(frame: &CommunicationValue) -> Result { let mut result = Map::new(); if frame.get_id() != 0 { result.insert("id".into(), Value::from(frame.get_id())); } result.insert( "type".into(), Value::String(frame.get_type_name().unwrap_or("Unknown").to_owned()), ); result.insert("data".into(), frame_data_to_json(frame)?); Ok(Value::Object(result)) } fn frame_data_to_json(frame: &CommunicationValue) -> Result { let map = frame.type_map().cloned().unwrap_or_else(TypeMap::latest); let mut result = Map::new(); for (id, value) in frame.data() { let name = map .data_type_name(id.0) .ok_or_else(|| format!("unknown data type id: {}", id.0))?; result.insert(name.to_owned(), data_to_json(value, &map)?); } Ok(Value::Object(result)) } fn data_to_json(value: &DataValue, map: &TypeMap) -> Result { Ok(match value { DataValue::BoolTrue => Value::Bool(true), DataValue::BoolFalse => Value::Bool(false), DataValue::Bool(value) => Value::Bool(*value), DataValue::SignedNumber(value) => number_to_json(*value)?, DataValue::UnsignedNumber(value) => { number_to_json(i128::try_from(*value).map_err(|_| "number exceeds JSON range")?)? } DataValue::Float(value) => serde_json::Number::from_f64(*value) .map(Value::Number) .ok_or("invalid float")?, DataValue::Str(value) => Value::String(value.clone()), DataValue::Bytes(value) => Value::Array(value.iter().copied().map(Value::from).collect()), DataValue::Array(values) => Value::Array( values .iter() .map(|value| data_to_json(value, map)) .collect::>()?, ), DataValue::Container(entries) => { let mut object = Map::new(); for (id, value) in entries { let name = map .data_type_name(id.0) .ok_or_else(|| format!("unknown nested data type id: {}", id.0))?; object.insert(name.to_owned(), data_to_json(value, map)?); } Value::Object(object) } DataValue::Null => Value::Null, _ => return Err("encrypted protocol values cannot cross the frontend bridge".into()), }) } fn number_to_json(value: i128) -> Result { i64::try_from(value) .map(Value::from) .map_err(|_| "number exceeds JSON range".into()) } fn current_millis() -> u64 { std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default() .as_millis() as u64 } #[cfg(test)] mod tests { use super::{decode_browser_base64, decode_sdk_bytes}; #[test] fn browser_base64_accepts_file_whitespace_and_missing_padding() { assert_eq!(decode_browser_base64(" AQID\r\n").unwrap(), [1, 2, 3]); assert_eq!(decode_browser_base64("AQI").unwrap(), [1, 2]); } #[test] fn sdk_bytes_accept_hex_and_base64() { assert_eq!(decode_sdk_bytes("0x01:02-ff").unwrap(), [1, 2, 255]); assert_eq!(decode_sdk_bytes("AQI=").unwrap(), [1, 2]); } } #[tauri::command] pub async fn mtp_request(type_name: String, data: Value, id: Option) -> Result { manager().request(&type_name, data, id).await } #[tauri::command] pub fn mtp_status() -> MtpSnapshot { manager().snapshot() } #[tauri::command] pub fn mtp_store_credentials(config: MtpConfig) -> Result<(), String> { let serialized = serde_json::to_string(&config).map_err(|error| error.to_string())?; android_store_config(&serialized)?; *manager().config.write().expect("config lock poisoned") = Some(config); Ok(()) } #[tauri::command] pub fn mtp_has_credentials() -> Result { android_has_config() } #[tauri::command] pub fn mtp_load_keyring() -> Option { manager() .config .read() .expect("config lock poisoned") .as_ref() .map(|config| config.keyring.clone()) } #[tauri::command] pub fn mtp_set_enabled(enabled: bool) -> Result<(), String> { android_set_enabled(enabled)?; if !enabled { manager().stop(); } Ok(()) } #[tauri::command] pub fn mtp_is_ignoring_battery_optimizations() -> Result { android_is_ignoring_battery_optimizations() } #[tauri::command] pub fn mtp_request_battery_exemption() -> Result<(), String> { android_request_battery_exemption() } #[tauri::command] pub fn mtp_set_ui_visible(visible: bool) { manager().set_ui_visible(visible); } #[tauri::command] pub fn mtp_post_message_notification( sender_id: u64, sender: String, body: String, avatar: Option, ) -> Result { #[cfg(target_os = "android")] { let avatar = avatar.as_deref().map(decode_browser_base64).transpose()?; android_notify(sender_id, &sender, &body, avatar.as_deref())?; Ok(true) } #[cfg(not(target_os = "android"))] { let _ = (sender_id, sender, body, avatar); Ok(false) } } #[cfg(not(target_os = "android"))] fn android_store_config(_: &str) -> Result<(), String> { Ok(()) } #[cfg(not(target_os = "android"))] fn android_has_config() -> Result { Ok(manager() .config .read() .expect("config lock poisoned") .is_some()) } #[cfg(not(target_os = "android"))] fn android_set_enabled(_: bool) -> Result<(), String> { Ok(()) } #[cfg(not(target_os = "android"))] fn android_status(_: &str) {} #[cfg(not(target_os = "android"))] fn android_notify(_: u64, _: &str, _: &str, _: Option<&[u8]>) -> Result<(), String> { Ok(()) } #[cfg(not(target_os = "android"))] fn android_cancel_notification(_: u64) -> Result<(), String> { Ok(()) } #[cfg(not(target_os = "android"))] fn android_is_ignoring_battery_optimizations() -> Result { Ok(true) } #[cfg(not(target_os = "android"))] fn android_request_battery_exemption() -> Result<(), String> { Ok(()) } #[cfg(target_os = "android")] mod android { use std::sync::OnceLock; use jni::{ objects::{GlobalRef, JClass, JObject, JString, JValue}, JNIEnv, JavaVM, }; use serde_json::Value; pub struct Host { vm: JavaVM, context: GlobalRef, bridge: GlobalRef, } static HOST: OnceLock = OnceLock::new(); pub fn attach(env: &mut JNIEnv, context: JObject) -> Result<(), String> { if HOST.get().is_some() { return Ok(()); } let class = env .find_class("net/tensamin/client/NativeMtpBridge") .map_err(|e| e.to_string())?; let bridge = env .get_static_field(class, "INSTANCE", "Lnet/tensamin/client/NativeMtpBridge;") .and_then(|value| value.l()) .map_err(|e| e.to_string())?; HOST.set(Host { vm: env.get_java_vm().map_err(|e| e.to_string())?, context: env.new_global_ref(context).map_err(|e| e.to_string())?, bridge: env.new_global_ref(bridge).map_err(|e| e.to_string())?, }) .map_err(|_| "Android MTP host is already attached".to_string()) } fn with_env( call: impl FnOnce(&mut JNIEnv, &Host) -> Result, ) -> Result { let host = HOST.get().ok_or("Android MTP host is not attached")?; let mut env = host .vm .attach_current_thread_as_daemon() .map_err(|e| e.to_string())?; call(&mut env, host) } pub fn store_config(config: &str) -> Result<(), String> { with_env(|env, host| { let value = env.new_string(config).map_err(|e| e.to_string())?; env.call_method( host.bridge.as_obj(), "storeConfig", "(Landroid/content/Context;Ljava/lang/String;)V", &[ JValue::Object(host.context.as_obj()), JValue::Object(&value), ], ) .map_err(|e| e.to_string())?; Ok(()) }) } pub fn has_config() -> Result { with_env(|env, host| { env.call_method( host.bridge.as_obj(), "hasConfig", "(Landroid/content/Context;)Z", &[JValue::Object(host.context.as_obj())], ) .and_then(|value| value.z()) .map_err(|e| e.to_string()) }) } pub fn set_enabled(enabled: bool) -> Result<(), String> { with_env(|env, host| { env.call_method( host.bridge.as_obj(), "setServiceEnabled", "(Landroid/content/Context;Z)V", &[ JValue::Object(host.context.as_obj()), JValue::Bool(enabled.into()), ], ) .map_err(|e| e.to_string())?; Ok(()) }) } pub fn status(status: &str) { let _ = with_env(|env, host| { let status = env.new_string(status).map_err(|e| e.to_string())?; env.call_method( host.bridge.as_obj(), "updateServiceStatus", "(Landroid/content/Context;Ljava/lang/String;)V", &[ JValue::Object(host.context.as_obj()), JValue::Object(&status), ], ) .map_err(|e| e.to_string())?; Ok(()) }); } pub fn notify( sender_id: u64, sender: &str, body: &str, avatar: Option<&[u8]>, ) -> Result<(), String> { with_env(|env, host| { let sender = env.new_string(sender).map_err(|e| e.to_string())?; let body = env.new_string(body).map_err(|e| e.to_string())?; let avatar = env .byte_array_from_slice(avatar.unwrap_or_default()) .map_err(|e| e.to_string())?; env.call_method( host.bridge.as_obj(), "postMessageNotification", "(Landroid/content/Context;JLjava/lang/String;Ljava/lang/String;[B)V", &[ JValue::Object(host.context.as_obj()), JValue::Long(sender_id as i64), JValue::Object(&sender), JValue::Object(&body), JValue::Object(&avatar), ], ) .map_err(|e| e.to_string())?; Ok(()) }) } pub fn cancel_notification(sender_id: u64) -> Result<(), String> { with_env(|env, host| { env.call_method( host.bridge.as_obj(), "cancelMessageNotification", "(Landroid/content/Context;J)V", &[ JValue::Object(host.context.as_obj()), JValue::Long(sender_id as i64), ], ) .map_err(|e| e.to_string())?; Ok(()) }) } pub fn is_ignoring_battery_optimizations() -> Result { with_env(|env, host| { env.call_method( host.bridge.as_obj(), "isIgnoringBatteryOptimizations", "(Landroid/content/Context;)Z", &[JValue::Object(host.context.as_obj())], ) .and_then(|value| value.z()) .map_err(|e| e.to_string()) }) } pub fn request_battery_exemption() -> Result<(), String> { with_env(|env, host| { env.call_method( host.bridge.as_obj(), "requestBatteryExemption", "(Landroid/content/Context;)V", &[JValue::Object(host.context.as_obj())], ) .map_err(|e| e.to_string())?; Ok(()) }) } #[no_mangle] pub extern "system" fn Java_net_tensamin_client_NativeMtpBridge_nativeAttach( mut env: JNIEnv, _class: JClass, context: JObject, ) { let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| attach(&mut env, context))); } #[no_mangle] pub extern "system" fn Java_net_tensamin_client_NativeMtpBridge_nativeStart( mut env: JNIEnv, _class: JClass, config: JString, ) { let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { let config: String = env.get_string(&config).map_err(|e| e.to_string())?.into(); let config = serde_json::from_str(&config).map_err(|e| e.to_string())?; super::manager().configure_and_start(config); Ok::<_, String>(()) })); } #[no_mangle] pub extern "system" fn Java_net_tensamin_client_NativeMtpBridge_nativeStop( _env: JNIEnv, _class: JClass, ) { let _ = std::panic::catch_unwind(|| super::manager().stop()); } #[no_mangle] pub extern "system" fn Java_net_tensamin_client_NativeMtpBridge_nativeSetUiState( _env: JNIEnv, _class: JClass, visible: jni::sys::jboolean, ) { super::manager().set_ui_visible(visible != 0); } #[no_mangle] pub extern "system" fn Java_net_tensamin_client_NativeMtpBridge_nativeLog( mut env: JNIEnv, _class: JClass, level: jni::sys::jint, message: JString, details: JString, ) { let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { let message: String = env.get_string(&message).map_err(|e| e.to_string())?.into(); let details: String = env.get_string(&details).map_err(|e| e.to_string())?.into(); super::manager().log( level.clamp(0, 3) as u8, message, (!details.is_empty()).then(|| Value::String(details)), ); Ok::<_, String>(()) })); } } #[cfg(target_os = "android")] use android::{ cancel_notification as android_cancel_notification, has_config as android_has_config, is_ignoring_battery_optimizations as android_is_ignoring_battery_optimizations, notify as android_notify, request_battery_exemption as android_request_battery_exemption, set_enabled as android_set_enabled, status as android_status, store_config as android_store_config, };