Skip to content

Commit 6b76b11

Browse files
committed
android: add scan.
1 parent baba54a commit 6b76b11

File tree

9 files changed

+9123
-5
lines changed

9 files changed

+9123
-5
lines changed

Cargo.toml

+4
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ windows = { version = "0.48.0", features = [
5454
bluer = { version = "0.16.1", features = ["bluetoothd"] }
5555
tokio = { version = "1.20.1", features = ["rt-multi-thread"] }
5656

57+
[target.'cfg(target_os = "android")'.dependencies]
58+
jni-glue = { git = "https://github.com/akiles/jni-bindgen", rev = "1ab6c3cb29ffcca01b269fc6c23900da6668288f" }
59+
futures-channel = "0.3.24"
60+
5761
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies]
5862
async-broadcast = "0.5.1"
5963
objc = "0.2.7"

src/adapter.rs

+13
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,21 @@ use crate::{sys, AdapterEvent, AdvertisingDevice, ConnectionEvent, Device, Devic
1111
pub struct Adapter(sys::adapter::AdapterImpl);
1212

1313
impl Adapter {
14+
/// Creates an interface to the default Bluetooth adapter for the system.
15+
///
16+
/// # Safety
17+
///
18+
/// - `java_vm` must be a valid JNI `JavaVM` pointer to a VM that will stay alive for the entire duration the `Adapter` or any structs obtained from it are live.
19+
/// - `bluetooth_manager` must be a valid global reference to an `android.bluetooth.BluetoothManager` instance, from the `java_vm` VM.
20+
/// - The `Adapter` takes ownership of the global reference and will delete it with the `DeleteGlobalRef` JNI call when dropped. You must not do that yourself.
21+
#[cfg(target_os = "android")]
22+
pub unsafe fn new(java_vm: *mut jni_glue::sys::JavaVM, bluetooth_manager: jni_glue::sys::jobject) -> Result<Self> {
23+
sys::adapter::AdapterImpl::new(java_vm, bluetooth_manager).map(Self)
24+
}
25+
1426
/// Creates an interface to the default Bluetooth adapter for the system
1527
#[inline]
28+
#[cfg(not(target_os = "android"))]
1629
pub async fn default() -> Option<Self> {
1730
sys::adapter::AdapterImpl::default().await.map(Adapter)
1831
}

src/android/adapter.rs

+217-5
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,57 @@
1+
use std::collections::HashMap;
2+
use std::pin::Pin;
3+
use std::sync::atomic::{AtomicI32, Ordering};
4+
use std::sync::{Arc, Mutex};
5+
use std::task::{Context, Poll};
6+
7+
use futures_channel::mpsc::{Receiver, Sender};
18
use futures_core::Stream;
29
use futures_lite::{stream, StreamExt};
10+
use jni_glue::{Argument, ByteArray, Env, Global, Local, PrimitiveArray, VM};
11+
use tracing::{info, warn};
312
use uuid::Uuid;
413

5-
use crate::{AdapterEvent, AdvertisingDevice, ConnectionEvent, Device, DeviceId, Result};
14+
use super::bindings::android::bluetooth::le::{BluetoothLeScanner, ScanResult};
15+
use super::bindings::android::bluetooth::{BluetoothAdapter, BluetoothManager};
16+
use super::bindings::android::os::ParcelUuid;
17+
use super::bindings::com::github::alexmoon::bluest::android::BluestScanCallback;
18+
use super::device::DeviceImpl;
19+
use super::JavaIterator;
20+
use crate::android::bindings::java::util::Map_Entry;
21+
use crate::util::defer;
22+
use crate::{
23+
AdapterEvent, AdvertisementData, AdvertisingDevice, ConnectionEvent, Device, DeviceId, ManufacturerData, Result,
24+
};
25+
26+
struct AdapterInner {
27+
manager: Global<BluetoothManager>,
28+
_adapter: Global<BluetoothAdapter>,
29+
le_scanner: Global<BluetoothLeScanner>,
30+
}
631

732
#[derive(Clone)]
8-
pub struct AdapterImpl {}
33+
pub struct AdapterImpl {
34+
inner: Arc<AdapterInner>,
35+
}
36+
937
impl AdapterImpl {
10-
pub async fn default() -> Option<Self> {
11-
Some(Self {})
38+
pub unsafe fn new(vm: *mut jni_glue::sys::JavaVM, manager: jni_glue::sys::jobject) -> Result<Self> {
39+
let vm = VM::from_raw(vm);
40+
let manager: Global<BluetoothManager> = Global::from_raw(vm, manager);
41+
42+
vm.with_env(|env| {
43+
let local_manager = manager.with(env);
44+
let adapter = local_manager.getAdapter()?.unwrap();
45+
let le_scanner = adapter.getBluetoothLeScanner()?.unwrap();
46+
47+
Ok(Self {
48+
inner: Arc::new(AdapterInner {
49+
_adapter: adapter.as_global(),
50+
le_scanner: le_scanner.as_global(),
51+
manager: manager.clone(),
52+
}),
53+
})
54+
})
1255
}
1356

1457
pub(crate) async fn events(&self) -> Result<impl Stream<Item = Result<AdapterEvent>> + Send + Unpin + '_> {
@@ -35,7 +78,27 @@ impl AdapterImpl {
3578
&'a self,
3679
_services: &'a [Uuid],
3780
) -> Result<impl Stream<Item = AdvertisingDevice> + Send + Unpin + 'a> {
38-
Ok(stream::empty()) // TODO
81+
self.inner.manager.vm().with_env(|env| {
82+
let receiver = SCAN_CALLBACKS.allocate();
83+
let callback = BluestScanCallback::new(env, receiver.id)?;
84+
let callback_global = callback.as_global();
85+
let scanner = self.inner.le_scanner.with(env);
86+
scanner.startScan_ScanCallback(&**callback)?;
87+
88+
let guard = defer(move || {
89+
self.inner.manager.vm().with_env(|env| {
90+
let callback = callback_global.with(env);
91+
let scanner = self.inner.le_scanner.with(env);
92+
scanner.stopScan_ScanCallback(&**callback).unwrap();
93+
info!("stopped scan");
94+
});
95+
});
96+
97+
Ok(receiver.map(move |x| {
98+
let _guard = &guard;
99+
x
100+
}))
101+
})
39102
}
40103

41104
pub async fn discover_devices<'a>(
@@ -91,3 +154,152 @@ impl std::fmt::Debug for AdapterImpl {
91154
f.debug_tuple("Adapter").finish()
92155
}
93156
}
157+
158+
static SCAN_CALLBACKS: CallbackRouter<AdvertisingDevice> = CallbackRouter::new();
159+
160+
struct CallbackRouter<T: Send + 'static> {
161+
map: Mutex<Option<HashMap<i32, Sender<T>>>>,
162+
next_id: AtomicI32,
163+
}
164+
165+
impl<T: Send + 'static> CallbackRouter<T> {
166+
const fn new() -> Self {
167+
Self {
168+
map: Mutex::new(None),
169+
next_id: AtomicI32::new(0),
170+
}
171+
}
172+
173+
fn allocate(&'static self) -> CallbackReceiver<T> {
174+
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
175+
let (sender, receiver) = futures_channel::mpsc::channel(16);
176+
self.map
177+
.lock()
178+
.unwrap()
179+
.get_or_insert_with(Default::default)
180+
.insert(id, sender);
181+
182+
CallbackReceiver {
183+
router: self,
184+
id,
185+
receiver,
186+
}
187+
}
188+
189+
fn callback(&'static self, id: i32, val: T) {
190+
if let Some(sender) = self.map.lock().unwrap().as_mut().unwrap().get_mut(&id) {
191+
if let Err(e) = sender.try_send(val) {
192+
warn!("failed to send scan callback: {:?}", e)
193+
}
194+
}
195+
}
196+
}
197+
198+
struct CallbackReceiver<T: Send + 'static> {
199+
router: &'static CallbackRouter<T>,
200+
id: i32,
201+
receiver: Receiver<T>,
202+
}
203+
204+
impl<T: Send + 'static> Stream for CallbackReceiver<T> {
205+
type Item = T;
206+
207+
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
208+
// safety: this is just a manually-written pin projection.
209+
let receiver = unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().receiver) };
210+
receiver.poll_next(cx)
211+
}
212+
}
213+
214+
impl<T: Send> Drop for CallbackReceiver<T> {
215+
fn drop(&mut self) {
216+
self.router.map.lock().unwrap().as_mut().unwrap().remove(&self.id);
217+
}
218+
}
219+
220+
#[no_mangle]
221+
pub extern "system" fn Java_com_github_alexmoon_bluest_android_BluestScanCallback_nativeOnScanResult(
222+
env: Env<'_>,
223+
_class: *mut (), // self class, ignore
224+
id: i32,
225+
callback_type: i32,
226+
scan_result: Argument<ScanResult>,
227+
) {
228+
let scan_result = unsafe { scan_result.with_unchecked(env) }.unwrap();
229+
230+
tracing::info!("got callback! {} {}", id, callback_type);
231+
232+
let scan_record = scan_result.getScanRecord().unwrap().unwrap();
233+
let device = scan_result.getDevice().unwrap().unwrap();
234+
235+
fn convert_uuid(uuid: Local<'_, ParcelUuid>) -> Uuid {
236+
let uuid = uuid.getUuid().unwrap().unwrap();
237+
let lsb = uuid.getLeastSignificantBits().unwrap() as u64;
238+
let msb = uuid.getMostSignificantBits().unwrap() as u64;
239+
Uuid::from_u64_pair(msb, lsb)
240+
}
241+
242+
let address = device.getAddress().unwrap().unwrap().to_string_lossy();
243+
let rssi = scan_result.getRssi().unwrap();
244+
let is_connectable = scan_result.isConnectable().unwrap();
245+
let local_name = scan_record.getDeviceName().unwrap().map(|s| s.to_string_lossy());
246+
let tx_power_level = scan_record.getTxPowerLevel().unwrap();
247+
248+
// Services
249+
let mut services = Vec::new();
250+
if let Some(uuids) = scan_record.getServiceUuids().unwrap() {
251+
for uuid in JavaIterator(uuids.iterator().unwrap().unwrap()) {
252+
services.push(convert_uuid(uuid.cast().unwrap()))
253+
}
254+
}
255+
256+
// Service data
257+
let mut service_data = HashMap::new();
258+
let sd = scan_record.getServiceData().unwrap().unwrap();
259+
let sd = sd.entrySet().unwrap().unwrap();
260+
for entry in JavaIterator(sd.iterator().unwrap().unwrap()) {
261+
let entry: Local<Map_Entry> = entry.cast().unwrap();
262+
let key: Local<ParcelUuid> = entry.getKey().unwrap().unwrap().cast().unwrap();
263+
let val: Local<ByteArray> = entry.getValue().unwrap().unwrap().cast().unwrap();
264+
service_data.insert(convert_uuid(key), val.as_vec().into_iter().map(|i| i as u8).collect());
265+
}
266+
267+
// Manufacturer data
268+
let mut manufacturer_data = None;
269+
let msd = scan_record.getManufacturerSpecificData().unwrap().unwrap();
270+
// TODO there can be multiple manufacturer data entries, but the bluest API only supports one. So grab just the first.
271+
if msd.size().unwrap() != 0 {
272+
let val: Local<'_, ByteArray> = msd.valueAt(0).unwrap().unwrap().cast().unwrap();
273+
manufacturer_data = Some(ManufacturerData {
274+
company_id: msd.keyAt(0).unwrap() as _,
275+
data: val.as_vec().into_iter().map(|i| i as u8).collect(),
276+
});
277+
}
278+
279+
let device_id = DeviceId(address);
280+
281+
let d = AdvertisingDevice {
282+
device: Device(DeviceImpl { id: device_id }),
283+
adv_data: AdvertisementData {
284+
is_connectable,
285+
local_name,
286+
manufacturer_data, // TODO, SparseArray is cursed.
287+
service_data,
288+
services,
289+
tx_power_level: Some(tx_power_level as _),
290+
},
291+
rssi: Some(rssi as _),
292+
};
293+
SCAN_CALLBACKS.callback(id, d);
294+
}
295+
296+
#[no_mangle]
297+
pub extern "system" fn Java_com_github_alexmoon_bluest_android_BluestScanCallback_nativeOnScanFailed(
298+
_env: Env<'_>,
299+
_class: *mut (), // self class, ignore
300+
id: i32,
301+
error_code: i32,
302+
) {
303+
tracing::error!("got scan fail! {} {}", id, error_code);
304+
todo!()
305+
}

0 commit comments

Comments
 (0)