1use std::sync::atomic::{AtomicBool, Ordering};
2use std::sync::Arc;
3use std::time::{Duration, SystemTime, UNIX_EPOCH};
4
5use flume::Sender;
6
7use crate::{CoreMsg, FfiApp, FipsBleCommand, FipsBleEvent};
8
9#[derive(uniffi::Object)]
10pub struct FfiFipsBle {
11 runtime: tokio::runtime::Runtime,
12 adapter: Arc<fips_core::transport::ble::host::HostBleAdapter>,
13 core_tx: Sender<CoreMsg>,
14 closed: AtomicBool,
15 ownership: crate::core::HostBleOwnership,
16}
17
18#[derive(Debug, Clone, uniffi::Error)]
19pub enum FipsBleBridgeError {
20 Initialization(String),
21 Teardown(String),
22}
23
24impl std::fmt::Display for FipsBleBridgeError {
25 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 match self {
27 Self::Initialization(message) | Self::Teardown(message) => formatter.write_str(message),
28 }
29 }
30}
31
32impl std::error::Error for FipsBleBridgeError {}
33
34#[uniffi::export]
35impl FfiFipsBle {
36 #[uniffi::constructor]
37 pub fn new(app: Arc<FfiApp>) -> Result<Arc<Self>, FipsBleBridgeError> {
38 Self::connect(app.background_tx.clone(), Duration::from_secs(2))
39 }
40
41 pub fn next_command(&self, timeout_ms: u64) -> Option<FipsBleCommand> {
42 let timeout = Duration::from_millis(timeout_ms.clamp(1, 60_000));
43 self.runtime.block_on(async {
44 tokio::time::timeout(timeout, self.adapter.next_command())
45 .await
46 .ok()
47 .flatten()
48 .map(Into::into)
49 })
50 }
51
52 pub fn emit(&self, event: FipsBleEvent) -> bool {
53 self.runtime
54 .block_on(self.adapter.emit(event.into()))
55 .is_ok()
56 }
57
58 pub fn detach(&self) -> Result<(), FipsBleBridgeError> {
59 self.detach_with_timeout(Duration::from_secs(2))
60 }
61
62 pub fn background_activity(&self, generation: u64, active: bool) -> bool {
64 let (reply_tx, reply_rx) = flume::bounded(1);
65 self.core_tx
66 .send(CoreMsg::HostBleActivity {
67 ownership: self.ownership.clone(),
68 generation,
69 active,
70 reply_tx,
71 })
72 .is_ok()
73 && reply_rx.recv().unwrap_or(false)
74 }
75}
76
77impl FfiFipsBle {
78 pub(crate) fn connect(
79 core_tx: Sender<CoreMsg>,
80 timeout: Duration,
81 ) -> Result<Arc<Self>, FipsBleBridgeError> {
82 let runtime = tokio::runtime::Builder::new_multi_thread()
83 .worker_threads(1)
84 .enable_time()
85 .build()
86 .map_err(|error| {
87 FipsBleBridgeError::Initialization(format!(
88 "Could not create the FIPS BLE runtime: {error}"
89 ))
90 })?;
91 let local_token = format!(
92 "iris-{}",
93 SystemTime::now()
94 .duration_since(UNIX_EPOCH)
95 .unwrap_or_default()
96 .as_nanos()
97 );
98 let (io, adapter) = runtime
99 .block_on(async {
100 fips_core::transport::ble::host::HostBleIo::channel("mobile", local_token, 256)
101 })
102 .map_err(|error| {
103 FipsBleBridgeError::Initialization(format!(
104 "Could not create the FIPS BLE bridge: {error}"
105 ))
106 })?;
107 let attachment =
111 crate::core::HostBleAttachment::new(io).with_background(cfg!(target_os = "ios"));
112 let ownership = attachment.ownership();
113 let bridge = Arc::new(Self {
116 runtime,
117 adapter: Arc::new(adapter),
118 core_tx,
119 closed: AtomicBool::new(false),
120 ownership,
121 });
122 let (reply_tx, reply_rx) = flume::bounded(1);
123 bridge
124 .core_tx
125 .send(CoreMsg::AttachHostBle {
126 attachment,
127 reply_tx,
128 })
129 .map_err(|error| {
130 FipsBleBridgeError::Initialization(format!(
131 "Could not request the FIPS BLE bridge attachment: {error}"
132 ))
133 })?;
134 reply_rx
135 .recv_timeout(timeout)
136 .map_err(|error| {
137 FipsBleBridgeError::Initialization(format!(
138 "The Iris core did not attach the FIPS BLE bridge: {error}"
139 ))
140 })?
141 .map_err(FipsBleBridgeError::Initialization)?;
142 Ok(bridge)
143 }
144
145 pub(crate) fn detach_with_timeout(&self, timeout: Duration) -> Result<(), FipsBleBridgeError> {
146 if self.closed.load(Ordering::Acquire) {
147 return Ok(());
148 }
149 self.ownership.cancel();
150 let (reply_tx, reply_rx) = flume::bounded(1);
151 self.core_tx
152 .send(CoreMsg::DetachHostBle {
153 ownership: self.ownership.clone(),
154 reply_tx,
155 })
156 .map_err(|error| {
157 FipsBleBridgeError::Teardown(format!(
158 "Could not request FIPS BLE teardown: {error}"
159 ))
160 })?;
161 reply_rx.recv_timeout(timeout).map_err(|error| {
162 FipsBleBridgeError::Teardown(format!(
163 "The Iris core has not acknowledged FIPS BLE teardown: {error}"
164 ))
165 })?;
166 self.closed.store(true, Ordering::Release);
167 Ok(())
168 }
169}
170
171impl Drop for FfiFipsBle {
172 fn drop(&mut self) {
173 if !self.closed.load(Ordering::Acquire) {
174 self.ownership.cancel();
175 let (reply_tx, _) = flume::bounded(1);
176 let _ = self.core_tx.send(CoreMsg::DetachHostBle {
177 ownership: self.ownership.clone(),
178 reply_tx,
179 });
180 }
181 }
182}
183
184impl From<fips_core::transport::ble::host::HostBleCommand> for FipsBleCommand {
185 fn from(command: fips_core::transport::ble::host::HostBleCommand) -> Self {
186 use fips_core::transport::ble::host::HostBleCommand as Host;
187 match command {
188 Host::Listen {
189 request_id,
190 preferred_psm,
191 } => Self::Listen {
192 request_id,
193 preferred_psm,
194 },
195 Host::StopListening => Self::StopListening,
196 Host::StartAdvertising {
197 request_id,
198 bootstrap,
199 } => Self::StartAdvertising {
200 request_id,
201 bootstrap,
202 },
203 Host::StopAdvertising { request_id } => Self::StopAdvertising { request_id },
204 Host::StartScanning { request_id } => Self::StartScanning { request_id },
205 Host::StopScanning => Self::StopScanning,
206 Host::Connect {
207 request_id,
208 peer_token,
209 psm,
210 } => Self::Connect {
211 request_id,
212 peer_token,
213 psm,
214 },
215 Host::Write {
216 request_id,
217 connection_id,
218 bytes,
219 } => Self::Write {
220 request_id,
221 connection_id,
222 bytes,
223 },
224 Host::Close { connection_id } => Self::Close { connection_id },
225 }
226 }
227}
228
229impl From<FipsBleEvent> for fips_core::transport::ble::host::HostBleEvent {
230 fn from(event: FipsBleEvent) -> Self {
231 use fips_core::transport::ble::host::HostBleEvent as Host;
232 match event {
233 FipsBleEvent::Listening { request_id, psm } => Host::Listening { request_id, psm },
234 FipsBleEvent::AdvertisingStarted { request_id } => {
235 Host::AdvertisingStarted { request_id }
236 }
237 FipsBleEvent::AdvertisingStopped { request_id } => {
238 Host::AdvertisingStopped { request_id }
239 }
240 FipsBleEvent::ScanningStarted { request_id } => Host::ScanningStarted { request_id },
241 FipsBleEvent::PeerDiscovered {
242 peer_token,
243 bootstrap,
244 } => Host::PeerDiscovered {
245 peer_token,
246 bootstrap,
247 },
248 FipsBleEvent::Connected {
249 request_id,
250 connection_id,
251 peer_token,
252 send_segment_mtu,
253 receive_segment_mtu,
254 } => Host::Connected {
255 request_id,
256 connection_id,
257 peer_token,
258 send_segment_mtu,
259 receive_segment_mtu,
260 },
261 FipsBleEvent::IncomingConnection {
262 connection_id,
263 peer_token,
264 send_segment_mtu,
265 receive_segment_mtu,
266 } => Host::IncomingConnection {
267 connection_id,
268 peer_token,
269 send_segment_mtu,
270 receive_segment_mtu,
271 },
272 FipsBleEvent::BytesReceived {
273 connection_id,
274 bytes,
275 } => Host::BytesReceived {
276 connection_id,
277 bytes,
278 },
279 FipsBleEvent::WriteCompleted { request_id } => Host::WriteCompleted { request_id },
280 FipsBleEvent::Disconnected {
281 connection_id,
282 reason,
283 } => Host::Disconnected {
284 connection_id,
285 reason,
286 },
287 FipsBleEvent::Failed {
288 request_id,
289 message,
290 } => Host::Failed {
291 request_id,
292 message,
293 },
294 }
295 }
296}