Skip to main content

android_ble/
adapter.rs

1// Some portions of this code is orginally written by <https://github.com/Dirbaio>.
2
3use std::collections::HashMap;
4use std::sync::{Arc, OnceLock};
5
6use futures_core::Stream;
7use futures_lite::{stream, StreamExt};
8use java_spaghetti::{ByteArray, Env, Global, Local, Null, Ref};
9use log::{debug, warn};
10use uuid::Uuid;
11
12use super::async_util::StreamUntil;
13use super::bindings::android::bluetooth::le::{
14    ScanCallback, ScanFilter_Builder, ScanResult, ScanSettings, ScanSettings_Builder,
15};
16use super::bindings::android::bluetooth::{
17    BluetoothAdapter, BluetoothDevice, BluetoothGattCallback, BluetoothManager, BluetoothProfile,
18};
19use super::bindings::android::content::Context as AndroidContext;
20use super::bindings::android::os::ParcelUuid;
21use super::bindings::java::lang::String as JString;
22use super::bindings::java::util::Map_Entry;
23use super::bindings::java::{self};
24use super::device::Device;
25use super::error::ErrorKind;
26use super::event_receiver::{EventReceiver, GlobalEvent};
27use super::gatt_tree::{BluetoothGattCallbackProxy, CachedWeak, GattTree};
28use super::jni::{ByteArrayExt, Monitor, VM};
29use super::vm_context::{
30    android_api_level, android_context, android_has_permission, jni_get_vm, jni_set_vm,
31    jni_with_env,
32};
33use crate::util::{defer, JavaIterator, OptionExt, UuidExt};
34use crate::{
35    AdapterEvent, AdvertisementData, AdvertisingDevice, ConnectionEvent, DeviceId, Error,
36    ManufacturerData, Result,
37};
38
39/// The system’s Bluetooth adapter interface.
40#[derive(Clone)]
41pub struct Adapter {
42    inner: Arc<AdapterInner>,
43}
44
45struct AdapterInner {
46    #[allow(unused)]
47    manager: Global<BluetoothManager>,
48    adapter: Global<BluetoothAdapter>,
49    global_event_receiver: Arc<EventReceiver>,
50    request_mtu_on_connect: bool,
51    allow_multiple_connections: bool,
52}
53
54static CONN_MUTEX: async_lock::Mutex<()> = async_lock::Mutex::new(());
55
56/// Configuration for creating an interface to the default Bluetooth adapter of the system.
57///
58/// By deafult, [ndk-context](https://docs.rs/ndk-context/0.1.1/ndk_context) is used for
59/// obtaining the JNI `JavaVM` pointer.
60pub struct AdapterConfig {
61    /// - `vm` must be a valid JNI `JavaVM` pointer to a VM that will stay alive for the current
62    ///   native library's lifetime. This is true for any library used by an Android application.
63    vm: *mut java_spaghetti::sys::JavaVM,
64    /// `manager` must be a valid global reference to an `android.bluetooth.BluetoothManager`
65    /// instance, from the `java_vm` VM.
66    manager: java_spaghetti::sys::jobject,
67
68    request_mtu_on_connect: bool,
69    allow_multiple_connections: bool,
70}
71
72impl AdapterConfig {
73    /// Creates a config for the default Bluetooth adapter for the system.
74    ///
75    /// You do not need to do this if you are using the `android_activity` crate for a native application.
76    ///
77    /// # Safety
78    ///
79    /// - `java_vm` must be a valid JNI `JavaVM` pointer to a VM that will stay alive for the current native
80    ///   library's lifetime. This is true for any library used by an Android application.
81    /// - `bluetooth_manager` must be a valid global reference to an `android.bluetooth.BluetoothManager`
82    ///   instance, from the `java_vm` VM.
83    /// - The `Adapter` takes ownership of the global reference and will delete it with the `DeleteGlobalRef`
84    ///   JNI call when dropped. You must not do that yourself.
85    pub unsafe fn new(
86        java_vm: *mut java_spaghetti::sys::JavaVM,
87        bluetooth_manager: java_spaghetti::sys::jobject,
88    ) -> Self {
89        Self {
90            vm: java_vm,
91            manager: bluetooth_manager,
92            request_mtu_on_connect: true,
93            allow_multiple_connections: true,
94        }
95    }
96
97    /// If enabled, this library will request the BLE ATT MTU to 517 bytes during [Adapter::connect_device].
98    /// See <https://developer.android.com/about/versions/14/behavior-changes-all#mtu-set-to-517>.
99    ///
100    /// If disabled, [crate::Characteristic::max_write_len] may always return `18`.
101    ///
102    /// This is enabled by default; disable it if the firmware of the device to be connected is problematic.
103    pub fn request_mtu_on_connect(mut self, enabled: bool) -> Self {
104        self.request_mtu_on_connect = enabled;
105        self
106    }
107
108    /// If enabled, connections with devices already connected outside this library instance will
109    /// be permitted. Note that another `android.bluetooth.BluetoothGatt` object will not be created
110    /// if the device is already connected in the current library instance.
111    ///
112    /// This is enabled by default; this should be okay on well-implemented Android API implementations,
113    /// but disabling it might improve Android compatibility.
114    pub fn allow_multiple_connections(mut self, enabled: bool) -> Self {
115        self.allow_multiple_connections = enabled;
116        self
117    }
118}
119
120impl Default for AdapterConfig {
121    fn default() -> Self {
122        jni_with_env(|env| {
123            let context = android_context().as_local(env);
124            let service_name = JString::from_env_str(env, AndroidContext::BLUETOOTH_SERVICE);
125            let manager = context
126                .getSystemService_String(service_name)
127                .unwrap()
128                .expect("Context.getSystemService() returned null for BLUETOOTH_SERVICE")
129                .cast::<BluetoothManager>()?
130                .as_global();
131            let config = unsafe { Self::new(jni_get_vm().as_raw(), manager.into_raw()) };
132            Ok::<_, Box<dyn std::error::Error>>(config)
133        })
134        .unwrap()
135    }
136}
137
138fn check_scan_permission() -> Result<(), crate::Error> {
139    let has_perm = if android_api_level() >= 31 {
140        if android_has_permission("android.permission.BLUETOOTH_SCAN") {
141            if !android_has_permission("android.permission.ACCESS_FINE_LOCATION") {
142                warn!("Please ensure `neverForLocation` is included in `android:usesPermissionFlags`.")
143            }
144            true // XXX
145        } else {
146            false
147        }
148    } else if android_api_level() >= 29 {
149        android_has_permission("android.permission.ACCESS_FINE_LOCATION")
150            && android_has_permission("android.permission.BLUETOOTH_ADMIN")
151    } else {
152        (android_has_permission("android.permission.ACCESS_COARSE_LOCATION")
153            || android_has_permission("android.permission.ACCESS_FINE_LOCATION"))
154            && android_has_permission("android.permission.BLUETOOTH_ADMIN")
155    };
156    if !has_perm {
157        return Err(crate::Error::new(
158            ErrorKind::NotAuthorized,
159            None,
160            "Bluetooth scanning permission is not granted",
161        ));
162    }
163    Ok(())
164}
165
166fn check_connection_permission() -> Result<(), crate::Error> {
167    if !android_has_permission(if android_api_level() >= 31 {
168        "android.permission.BLUETOOTH_CONNECT"
169    } else {
170        "android.permission.BLUETOOTH"
171    }) {
172        return Err(crate::Error::new(
173            ErrorKind::NotAuthorized,
174            None,
175            "Bluetooth connection permission is not granted",
176        ));
177    }
178    Ok(())
179}
180
181impl Adapter {
182    /// Creates an interface to a Bluetooth adapter using the default config.
183    pub async fn default() -> Result<Self> {
184        Adapter::with_config(AdapterConfig::default()).await
185    }
186
187    /// Creates an interface to a Bluetooth adapter. The `vm` pointer will be ignored
188    /// if this has been called previously.
189    pub async fn with_config(config: AdapterConfig) -> Result<Self> {
190        unsafe {
191            let vm = VM::from_raw(config.vm);
192            let _ = jni_set_vm(vm);
193
194            let manager: Global<BluetoothManager> = Global::from_raw(vm.into(), config.manager);
195
196            jni_with_env(|env| {
197                let local_manager = manager.as_ref(env);
198                let adapter = local_manager.getAdapter()?.non_null()?;
199                Ok(Self {
200                    inner: Arc::new(AdapterInner {
201                        adapter: adapter.as_global(),
202                        manager: manager.clone(),
203                        global_event_receiver: EventReceiver::build()?,
204                        request_mtu_on_connect: config.request_mtu_on_connect,
205                        allow_multiple_connections: config.allow_multiple_connections,
206                    }),
207                })
208            })
209        }
210    }
211
212    /// A stream of [AdapterEvent] which allows the application to identify when the adapter is enabled or disabled.
213    pub async fn events(
214        &self,
215    ) -> Result<impl Stream<Item = Result<AdapterEvent>> + Send + Unpin + '_> {
216        Ok(self
217            .inner
218            .global_event_receiver
219            .subscribe()
220            .await?
221            .filter_map(|event| {
222                if let GlobalEvent::AdapterStateChanged(val) = event {
223                    match val {
224                        BluetoothAdapter::STATE_ON => Some(AdapterEvent::Available),
225                        BluetoothAdapter::STATE_OFF => Some(AdapterEvent::Unavailable),
226                        _ => None, // XXX: process "turning on" and "turning off" events
227                    }
228                } else {
229                    None
230                }
231            })
232            .map(Ok))
233    }
234
235    /// Asynchronously blocks until the adapter is available.
236    pub async fn wait_available(&self) -> Result<()> {
237        while !self.is_available().await? {
238            let mut events = self.events().await?;
239            while let Some(Ok(event)) = events.next().await {
240                if event == AdapterEvent::Available {
241                    return Ok(());
242                }
243            }
244        }
245        Ok(())
246    }
247
248    /// Check if the adapter is available.
249    pub async fn is_available(&self) -> Result<bool> {
250        jni_with_env(|env| {
251            let adapter = self.inner.adapter.as_local(env);
252            adapter.isEnabled().map_err(|e| {
253                Error::new(ErrorKind::Internal, None, format!("isEnabled threw: {e:?}"))
254            })
255        })
256    }
257
258    /// Attempts to create the device identified by `id`.
259    pub async fn open_device(&self, id: &DeviceId) -> Result<Device> {
260        if let Some(dev) = self
261            .connected_devices()
262            .await?
263            .into_iter()
264            .find(|d| &d.id() == id)
265        {
266            return Ok(dev);
267        }
268        jni_with_env(|env| {
269            let adapter = self.inner.adapter.as_local(env);
270            let device = adapter
271                .getRemoteDevice_String(JString::from_env_str(env, &id.0))
272                .map_err(|e| {
273                    Error::new(
274                        ErrorKind::Internal,
275                        None,
276                        format!("getRemoteDevice threw: {e:?}"),
277                    )
278                })?
279                .non_null()?;
280            Ok(Device {
281                id: id.clone(),
282                device: device.as_global(),
283                connection: CachedWeak::new(),
284                once_connected: Arc::new(OnceLock::new()),
285            })
286        })
287    }
288
289    /// Finds all connected Bluetooth LE devices.
290    ///
291    /// NOTE: there might be BLE devices connected outside this library.
292    /// If [AdapterConfig::allow_multiple_connections] is set to true, this method will call
293    /// `BluetoothManager.getConnectedDevices()` and ensure GATT connections are created
294    /// for them in this library instance.
295    pub async fn connected_devices(&self) -> Result<Vec<Device>> {
296        check_connection_permission()?;
297        if self.inner.allow_multiple_connections {
298            let mut device_items = Vec::new();
299            jni_with_env(|env| {
300                let manager = self.inner.manager.as_ref(env);
301                let devices = manager
302                    .getConnectedDevices(BluetoothProfile::GATT)?
303                    .non_null()?;
304                let iter_devices = JavaIterator(devices.iterator()?.non_null()?);
305
306                for device in iter_devices.filter_map(|dev| dev.cast::<BluetoothDevice>().ok()) {
307                    let id = DeviceId(
308                        device
309                            .getAddress()?
310                            .non_null()?
311                            .to_string_lossy()
312                            .trim()
313                            .to_string(),
314                    );
315                    let device_item = Device {
316                        id,
317                        device: device.as_global(),
318                        connection: CachedWeak::new(),
319                        // NOTE: this makes the `connect_device` called later to discover services as if it's reconnected.
320                        once_connected: Arc::new(OnceLock::from(())),
321                    };
322                    device_items.push(device_item);
323                }
324                Ok::<_, crate::Error>(())
325            })?;
326            for device_item in &device_items {
327                if GattTree::find_connection(&device_item.id).is_none() {
328                    self.connect_device(device_item).await?;
329                }
330            }
331            Ok(device_items)
332        } else {
333            GattTree::registered_devices()
334        }
335    }
336
337    /// Finds all connected devices providing any service in `service_ids`.
338    pub async fn connected_devices_with_services(
339        &self,
340        service_ids: &[Uuid],
341    ) -> Result<Vec<Device>> {
342        let mut devices_found = Vec::new();
343        for device in self.connected_devices().await? {
344            device.discover_services().await?;
345            let device_services = device.services().await?;
346            if service_ids
347                .iter()
348                .any(|&id| device_services.iter().any(|serv| serv.uuid() == id))
349            {
350                devices_found.push(device);
351            }
352        }
353        Ok(devices_found)
354    }
355
356    /// Starts scanning for Bluetooth advertising packets.
357    ///
358    /// Returns a stream of [`AdvertisingDevice`] structs which contain the data from the advertising packet and the
359    /// [`Device`] which sent it. Scanning is automatically stopped when the stream is dropped. Inclusion of duplicate
360    /// packets is a platform-specific implementation detail.
361    ///
362    /// If `service_ids` is not empty, returns advertisements including at least one GATT service with a UUID in
363    /// `services`. Otherwise returns all advertisements.
364    pub async fn scan<'a>(
365        &'a self,
366        service_ids: &'a [Uuid],
367    ) -> Result<impl Stream<Item = AdvertisingDevice> + Send + Unpin + 'a> {
368        check_scan_permission()?;
369        let (start_receiver, stream) = jni_with_env(|env| {
370            let (start_sender, start_receiver) = async_channel::bounded(1);
371            let (device_sender, device_receiver) = async_channel::bounded(16);
372
373            let callback = ScanCallback::new_proxy(
374                env,
375                Arc::new(ScanCallbackProxy {
376                    device_sender,
377                    start_sender,
378                }),
379            )?;
380            let callback_global = callback.as_global();
381
382            let adapter = self.inner.adapter.as_ref(env);
383            let adapter_global = adapter.as_global();
384            let adapter = Monitor::new(&adapter);
385            let scanner = adapter.getBluetoothLeScanner()?.non_null()?;
386            let scanner_global = scanner.as_global();
387
388            let settings_builder = ScanSettings_Builder::new(env)?;
389            settings_builder.setScanMode(ScanSettings::SCAN_MODE_LOW_LATENCY)?;
390            let settings = settings_builder.build()?.non_null()?;
391
392            if !service_ids.is_empty() {
393                let filter_builder = ScanFilter_Builder::new(env)?;
394                let filter_list = java::util::ArrayList::new(env)?;
395                for uuid in service_ids {
396                    let uuid_string = JString::from_env_str(env, uuid.to_string());
397                    let parcel_uuid = ParcelUuid::fromString(env, uuid_string)?;
398                    filter_builder.setServiceUuid_ParcelUuid(parcel_uuid)?;
399                    let filter = filter_builder.build()?.non_null()?;
400                    filter_list.add_Object(filter)?;
401                }
402                scanner.startScan_List_ScanSettings_ScanCallback(
403                    filter_list,
404                    settings,
405                    callback,
406                )?;
407            } else {
408                scanner.startScan_List_ScanSettings_ScanCallback(Null, settings, callback)?;
409            };
410
411            let guard = defer(move || {
412                jni_with_env(|env| {
413                    let callback = callback_global.as_ref(env);
414                    let scanner = scanner_global.as_ref(env);
415                    if adapter_global.as_ref(env).isEnabled().unwrap_or(false) {
416                        match scanner.stopScan_ScanCallback(callback) {
417                            Ok(()) => debug!("stopped scan"),
418                            Err(e) => warn!("failed to stop scan: {:?}", e),
419                        };
420                    }
421                });
422            });
423
424            Ok::<_, crate::Error>((
425                start_receiver,
426                Box::pin(device_receiver).map(move |adv_dev| {
427                    let _guard = &guard;
428                    adv_dev
429                }),
430            ))
431        })?;
432
433        #[rustfmt::skip]
434        let stream = StreamUntil::create(
435            stream,
436            self.inner.global_event_receiver.subscribe().await?,
437            |event| {
438                matches!(
439                    event,
440                    GlobalEvent::DiscoveryFinished
441                        | GlobalEvent::AdapterStateChanged(BluetoothAdapter::STATE_OFF)
442                )
443            }
444        );
445
446        // Wait for scan started or failed.
447        match start_receiver.recv().await {
448            Ok(Ok(())) => Ok(stream),
449            Ok(Err(e)) => Err(e),
450            Err(e) => Err(Error::new(
451                ErrorKind::Internal,
452                None,
453                format!("receiving failed while waiting for start: {e:?}"),
454            )),
455        }
456    }
457
458    /// Finds Bluetooth devices providing any service in `services`.
459    ///
460    /// Returns a stream of [`Device`] structs with matching connected devices returned first. If the stream is not
461    /// dropped before all matching connected devices are consumed then scanning will begin for devices advertising any
462    /// of the `services`. Scanning will continue until the stream is dropped. Inclusion of duplicate devices is a
463    /// platform-specific implementation detail.
464    pub async fn discover_devices<'a>(
465        &'a self,
466        services: &'a [Uuid],
467    ) -> Result<impl Stream<Item = Result<Device>> + Send + Unpin + 'a> {
468        let connected = stream::iter(self.connected_devices_with_services(services).await?).map(Ok);
469
470        // try_unfold is used to ensure we do not start scanning until the connected devices have been consumed
471        let advertising = Box::pin(stream::try_unfold(None, |state| async {
472            let mut stream = match state {
473                Some(stream) => stream,
474                None => self.scan(services).await?,
475            };
476            Ok(stream.next().await.map(|x| (x.device, Some(stream))))
477        }));
478
479        Ok(connected.chain(advertising))
480    }
481
482    /// Connects to the [`Device`].
483    pub async fn connect_device(&self, device: &Device) -> Result<()> {
484        check_connection_permission()?;
485        let _conn_lock = CONN_MUTEX.lock().await;
486        if device.is_connected().await {
487            return Ok(());
488        }
489        if !self.inner.allow_multiple_connections && self.is_actually_connected(&device.id())? {
490            return Err(Error::new(
491                ErrorKind::ConnectionFailed,
492                None,
493                "device is connected outside the current `android_ble` library",
494            ));
495        }
496        let callback_hdl = BluetoothGattCallbackProxy::new(device.id());
497        jni_with_env(|env| {
498            let adapter = self.inner.adapter.as_ref(env);
499            let _lock = Monitor::new(&adapter);
500            let device_obj = device.device.as_local(env);
501            let proxy = BluetoothGattCallback::new_proxy(env, callback_hdl.clone())?;
502            let gatt = device_obj
503                .connectGatt_Context_boolean_BluetoothGattCallback(
504                    android_context().as_ref(env),
505                    false,
506                    proxy,
507                )
508                .map_err(|e| {
509                    Error::new(
510                        ErrorKind::Internal,
511                        None,
512                        format!("connectGatt threw: {e:?}"),
513                    )
514                })?
515                .non_null()?
516                .as_global();
517            GattTree::register_connection(
518                &device.id(),
519                gatt,
520                &callback_hdl,
521                &self.inner.global_event_receiver,
522            );
523            Ok::<_, crate::Error>(())
524        })?;
525        if !self.is_actually_connected(&device.id())? {
526            GattTree::wait_connection_available(&device.id()).await?;
527        }
528        if self.inner.request_mtu_on_connect {
529            let conn = GattTree::check_connection(&device.id())?;
530            let mtu_lock = conn.mtu_changed_received.lock().await;
531            jni_with_env(|env| {
532                let gatt = conn.gatt.as_ref(env);
533                let gatt = Monitor::new(&gatt);
534                gatt.requestMtu(517)?;
535                Ok::<_, crate::Error>(())
536            })?;
537            let _ = mtu_lock.wait_unlock().await;
538        }
539        // validates GATT tree API objects again upon reconnection
540        if device.once_connected.get().is_some() {
541            let _ = device.discover_services().await?;
542        }
543        let _ = device.once_connected.set(());
544        Ok(())
545    }
546
547    /// Disconnects from the [`Device`].
548    ///
549    /// XXX: manage to call this internally when all API wrapper objects for the device are dropped.
550    pub async fn disconnect_device(&self, device: &Device) -> Result<()> {
551        let _conn_lock = CONN_MUTEX.lock().await;
552        let Ok(conn) = device.get_connection() else {
553            return Ok(());
554        };
555        jni_with_env(|env| {
556            let adapter = self.inner.adapter.as_ref(env);
557            let _lock = Monitor::new(&adapter);
558            let gatt = &conn.gatt.as_ref(env);
559            let gatt = Monitor::new(gatt);
560            gatt.disconnect().map_err(|e| {
561                Error::new(
562                    ErrorKind::Internal,
563                    None,
564                    format!("BluetoothGatt.disconnect() threw: {e:?}"),
565                )
566            })?;
567            Ok::<_, crate::Error>(())
568        })?;
569        GattTree::deregister_connection(&device.id());
570        Ok(())
571    }
572
573    /// Monitors a device for connection/disconnection events.
574    ///
575    /// This monitors only devices connected/disconnected in this library instance,
576    /// even if [AdapterConfig::allow_multiple_connections] is set to true.
577    ///
578    /// This does not work with random address devices.
579    pub async fn device_connection_events<'a>(
580        &'a self,
581        device: &'a Device,
582    ) -> Result<impl Stream<Item = ConnectionEvent> + Send + Unpin + 'a> {
583        Ok(StreamUntil::create(
584            GattTree::connection_events()
585                .await
586                .filter_map(|(dev_id, ev)| {
587                    if dev_id == device.id() {
588                        Some(ev)
589                    } else {
590                        None
591                    }
592                }),
593            self.events().await?,
594            |e| matches!(e, Ok(AdapterEvent::Unavailable)),
595        ))
596    }
597
598    // NOTE: this returns true even if the device is connected outside this crate.
599    pub(crate) fn is_actually_connected(&self, dev_id: &DeviceId) -> Result<bool> {
600        jni_with_env(|env| {
601            let manager = self.inner.manager.as_ref(env);
602            let devices = manager
603                .getConnectedDevices(BluetoothProfile::GATT)?
604                .non_null()?;
605            let iter_devices = JavaIterator(devices.iterator()?.non_null()?);
606            for device in iter_devices.filter_map(|dev| dev.cast::<BluetoothDevice>().ok()) {
607                if dev_id.0 == device.getAddress()?.non_null()?.to_string_lossy().trim() {
608                    return Ok(true);
609                }
610            }
611            Ok(false)
612        })
613    }
614}
615
616impl PartialEq for Adapter {
617    fn eq(&self, _other: &Self) -> bool {
618        true
619    }
620}
621
622impl Eq for Adapter {}
623
624impl std::hash::Hash for Adapter {
625    fn hash<H: std::hash::Hasher>(&self, _state: &mut H) {}
626}
627
628impl std::fmt::Debug for Adapter {
629    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
630        f.debug_tuple("Adapter").finish()
631    }
632}
633
634struct ScanCallbackProxy {
635    start_sender: async_channel::Sender<Result<()>>,
636    device_sender: async_channel::Sender<AdvertisingDevice>,
637}
638
639impl super::callback::ScanCallbackProxy for ScanCallbackProxy {
640    fn onScanFailed<'env>(&self, _env: Env<'env>, error_code: i32) {
641        let e = Error::new(
642            ErrorKind::Internal,
643            None,
644            format!("Scan failed to start with error code {error_code}"),
645        );
646        if let Err(e) = self.start_sender.try_send(Err(e)) {
647            warn!("onScanFailed failed to send error: {e:?}");
648        }
649    }
650
651    fn onBatchScanResults<'env>(
652        &self,
653        env: Env<'env>,
654        scan_results: Option<Ref<'env, super::bindings::java::util::List>>,
655    ) {
656        let Some(scan_results) = scan_results else {
657            warn!("onBatchScanResults: ignoring null scan_results");
658            return;
659        };
660
661        if let Err(e) = self.on_scan_result_list(env, &scan_results) {
662            warn!("onBatchScanResults failed: {e:?}");
663        }
664    }
665
666    fn onScanResult<'env>(
667        &self,
668        env: Env<'env>,
669        _callback_type: i32,
670        scan_result: Option<Ref<'env, ScanResult>>,
671    ) {
672        let Some(scan_result) = scan_result else {
673            warn!("onScanResult: ignoring null scan_result");
674            return;
675        };
676
677        if let Err(e) = self.on_scan_result(env, &scan_result) {
678            warn!("onScanResult failed: {e:?}");
679        }
680    }
681}
682
683impl ScanCallbackProxy {
684    fn on_scan_result_list(
685        &self,
686        env: Env<'_>,
687        scan_results: &Ref<super::bindings::java::util::List>,
688    ) -> Result<()> {
689        for scan_result in JavaIterator(scan_results.iterator()?.non_null()?) {
690            let scan_result: Local<ScanResult> = scan_result.cast()?;
691            self.on_scan_result(env, &scan_result.as_ref())?;
692        }
693        Ok(())
694    }
695
696    fn on_scan_result(&self, _env: Env<'_>, scan_result: &Ref<ScanResult>) -> Result<()> {
697        let scan_record = scan_result.getScanRecord()?.non_null()?;
698        let device = scan_result.getDevice()?.non_null()?;
699
700        let address = device
701            .getAddress()?
702            .non_null()?
703            .to_string_lossy()
704            .trim()
705            .to_string();
706        let rssi = scan_result.getRssi()?;
707        let is_connectable = if android_api_level() >= 26 {
708            scan_result.isConnectable()?
709        } else {
710            true // XXX: try to check `eventType` via `ScanResult.toString()`
711        };
712        let local_name = scan_record.getDeviceName()?.map(|s| s.to_string_lossy());
713        let tx_power_level = scan_record.getTxPowerLevel()?;
714
715        // Services
716        let mut services = Vec::new();
717        if let Some(uuids) = scan_record.getServiceUuids()? {
718            for uuid in JavaIterator(uuids.iterator()?.non_null()?) {
719                services.push(Uuid::from_andriod_parcel(uuid.cast()?)?)
720            }
721        }
722
723        // Service data
724        let mut service_data = HashMap::new();
725        let sd = scan_record.getServiceData()?.non_null()?;
726        let sd = sd.entrySet()?.non_null()?;
727        for entry in JavaIterator(sd.iterator()?.non_null()?) {
728            let entry: Local<Map_Entry> = entry.cast()?;
729            let key: Local<ParcelUuid> = entry.getKey()?.non_null()?.cast()?;
730            let val: Local<ByteArray> = entry.getValue()?.non_null()?.cast()?;
731            service_data.insert(Uuid::from_andriod_parcel(key)?, val.as_vec_u8());
732        }
733
734        // Manufacturer data
735        let mut manufacturer_data = None;
736        let msd = scan_record.getManufacturerSpecificData()?.non_null()?;
737        // TODO: there can be multiple manufacturer data entries, but the API (compatible with bluest)
738        // only supports one. So grab just the first.
739        if msd.size()? != 0 {
740            let val: Local<'_, ByteArray> = msd.valueAt(0)?.non_null()?.cast()?;
741            manufacturer_data = Some(ManufacturerData {
742                company_id: msd.keyAt(0)? as _,
743                data: val.as_vec_u8(),
744            });
745        }
746
747        let device_id = DeviceId(address);
748
749        let d = AdvertisingDevice {
750            device: Device {
751                id: device_id.clone(),
752                device: device.as_global(),
753                connection: CachedWeak::new(),
754                once_connected: Arc::new(if GattTree::find_connection(&device_id).is_none() {
755                    OnceLock::new()
756                } else {
757                    OnceLock::from(()) // NOTE: this is unlikely to happen
758                }),
759            },
760            adv_data: AdvertisementData {
761                is_connectable,
762                local_name,
763                manufacturer_data, // TODO, SparseArray is cursed.
764                service_data,
765                services,
766                tx_power_level: Some(tx_power_level as _),
767            },
768            rssi: Some(rssi as _),
769        };
770
771        self.start_sender.try_send(Ok(())).ok();
772        self.device_sender.try_send(d).ok();
773
774        Ok(())
775    }
776}