script/dom/bluetooth/
bluetoothdevice.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::Cell;
6use std::collections::HashMap;
7use std::rc::Rc;
8
9use base::generic_channel::GenericSender;
10use bluetooth_traits::{
11    BluetoothCharacteristicMsg, BluetoothDescriptorMsg, BluetoothRequest, BluetoothResponse,
12    BluetoothServiceMsg,
13};
14use dom_struct::dom_struct;
15use js::realm::CurrentRealm;
16use profile_traits::generic_channel;
17
18use crate::conversions::Convert;
19use crate::dom::bindings::cell::DomRefCell;
20use crate::dom::bindings::codegen::Bindings::BluetoothDeviceBinding::BluetoothDeviceMethods;
21use crate::dom::bindings::codegen::Bindings::BluetoothRemoteGATTServerBinding::BluetoothRemoteGATTServerMethods;
22use crate::dom::bindings::error::{Error, ErrorResult};
23use crate::dom::bindings::inheritance::Castable;
24use crate::dom::bindings::reflector::{DomGlobal, reflect_dom_object_with_cx};
25use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom};
26use crate::dom::bindings::str::DOMString;
27use crate::dom::bluetooth::{AsyncBluetoothListener, Bluetooth, response_async};
28use crate::dom::bluetoothcharacteristicproperties::BluetoothCharacteristicProperties;
29use crate::dom::bluetoothremotegattcharacteristic::BluetoothRemoteGATTCharacteristic;
30use crate::dom::bluetoothremotegattdescriptor::BluetoothRemoteGATTDescriptor;
31use crate::dom::bluetoothremotegattserver::BluetoothRemoteGATTServer;
32use crate::dom::bluetoothremotegattservice::BluetoothRemoteGATTService;
33use crate::dom::eventtarget::EventTarget;
34use crate::dom::globalscope::GlobalScope;
35use crate::dom::promise::Promise;
36use crate::script_runtime::CanGc;
37
38#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
39#[derive(JSTraceable, MallocSizeOf)]
40struct AttributeInstanceMap {
41    service_map: DomRefCell<HashMap<String, Dom<BluetoothRemoteGATTService>>>,
42    characteristic_map: DomRefCell<HashMap<String, Dom<BluetoothRemoteGATTCharacteristic>>>,
43    descriptor_map: DomRefCell<HashMap<String, Dom<BluetoothRemoteGATTDescriptor>>>,
44}
45
46// https://webbluetoothcg.github.io/web-bluetooth/#bluetoothdevice
47#[dom_struct]
48pub(crate) struct BluetoothDevice {
49    eventtarget: EventTarget,
50    id: DOMString,
51    name: Option<DOMString>,
52    gatt: MutNullableDom<BluetoothRemoteGATTServer>,
53    context: Dom<Bluetooth>,
54    attribute_instance_map: AttributeInstanceMap,
55    watching_advertisements: Cell<bool>,
56}
57
58impl BluetoothDevice {
59    pub(crate) fn new_inherited(
60        id: DOMString,
61        name: Option<DOMString>,
62        context: &Bluetooth,
63    ) -> BluetoothDevice {
64        BluetoothDevice {
65            eventtarget: EventTarget::new_inherited(),
66            id,
67            name,
68            gatt: Default::default(),
69            context: Dom::from_ref(context),
70            attribute_instance_map: AttributeInstanceMap {
71                service_map: DomRefCell::new(HashMap::new()),
72                characteristic_map: DomRefCell::new(HashMap::new()),
73                descriptor_map: DomRefCell::new(HashMap::new()),
74            },
75            watching_advertisements: Cell::new(false),
76        }
77    }
78
79    pub(crate) fn new(
80        cx: &mut js::context::JSContext,
81        global: &GlobalScope,
82        id: DOMString,
83        name: Option<DOMString>,
84        context: &Bluetooth,
85    ) -> DomRoot<BluetoothDevice> {
86        reflect_dom_object_with_cx(
87            Box::new(BluetoothDevice::new_inherited(id, name, context)),
88            global,
89            cx,
90        )
91    }
92
93    pub(crate) fn get_gatt(
94        &self,
95        cx: &mut js::context::JSContext,
96    ) -> DomRoot<BluetoothRemoteGATTServer> {
97        self.gatt
98            .or_init(|| BluetoothRemoteGATTServer::new(cx, &self.global(), self))
99    }
100
101    fn get_context(&self) -> DomRoot<Bluetooth> {
102        DomRoot::from_ref(&self.context)
103    }
104
105    pub(crate) fn get_or_create_service(
106        &self,
107        cx: &mut js::context::JSContext,
108        service: &BluetoothServiceMsg,
109        server: &BluetoothRemoteGATTServer,
110    ) -> DomRoot<BluetoothRemoteGATTService> {
111        let service_map_ref = &self.attribute_instance_map.service_map;
112        let mut service_map = service_map_ref.borrow_mut();
113        if let Some(existing_service) = service_map.get(&service.instance_id) {
114            return DomRoot::from_ref(existing_service);
115        }
116        let bt_service = BluetoothRemoteGATTService::new(
117            cx,
118            &server.global(),
119            &server.Device(),
120            DOMString::from(service.uuid.clone()),
121            service.is_primary,
122            service.instance_id.clone(),
123        );
124        service_map.insert(service.instance_id.clone(), Dom::from_ref(&bt_service));
125        bt_service
126    }
127
128    pub(crate) fn get_or_create_characteristic(
129        &self,
130        cx: &mut js::context::JSContext,
131        characteristic: &BluetoothCharacteristicMsg,
132        service: &BluetoothRemoteGATTService,
133    ) -> DomRoot<BluetoothRemoteGATTCharacteristic> {
134        let characteristic_map_ref = &self.attribute_instance_map.characteristic_map;
135        let mut characteristic_map = characteristic_map_ref.borrow_mut();
136        if let Some(existing_characteristic) = characteristic_map.get(&characteristic.instance_id) {
137            return DomRoot::from_ref(existing_characteristic);
138        }
139        let properties = BluetoothCharacteristicProperties::new(
140            cx,
141            &service.global(),
142            characteristic.broadcast,
143            characteristic.read,
144            characteristic.write_without_response,
145            characteristic.write,
146            characteristic.notify,
147            characteristic.indicate,
148            characteristic.authenticated_signed_writes,
149            characteristic.reliable_write,
150            characteristic.writable_auxiliaries,
151        );
152        let bt_characteristic = BluetoothRemoteGATTCharacteristic::new(
153            cx,
154            &service.global(),
155            service,
156            DOMString::from(characteristic.uuid.clone()),
157            &properties,
158            characteristic.instance_id.clone(),
159        );
160        characteristic_map.insert(
161            characteristic.instance_id.clone(),
162            Dom::from_ref(&bt_characteristic),
163        );
164        bt_characteristic
165    }
166
167    pub(crate) fn is_represented_device_null(&self) -> bool {
168        let (sender, receiver) =
169            generic_channel::channel(self.global().time_profiler_chan().clone()).unwrap();
170        self.get_bluetooth_thread()
171            .send(BluetoothRequest::IsRepresentedDeviceNull(
172                self.Id().to_string(),
173                sender,
174            ))
175            .unwrap();
176        receiver.recv().unwrap()
177    }
178
179    pub(crate) fn get_or_create_descriptor(
180        &self,
181        cx: &mut js::context::JSContext,
182        descriptor: &BluetoothDescriptorMsg,
183        characteristic: &BluetoothRemoteGATTCharacteristic,
184    ) -> DomRoot<BluetoothRemoteGATTDescriptor> {
185        let descriptor_map_ref = &self.attribute_instance_map.descriptor_map;
186        let mut descriptor_map = descriptor_map_ref.borrow_mut();
187        if let Some(existing_descriptor) = descriptor_map.get(&descriptor.instance_id) {
188            return DomRoot::from_ref(existing_descriptor);
189        }
190        let bt_descriptor = BluetoothRemoteGATTDescriptor::new(
191            cx,
192            &characteristic.global(),
193            characteristic,
194            DOMString::from(descriptor.uuid.clone()),
195            descriptor.instance_id.clone(),
196        );
197        descriptor_map.insert(
198            descriptor.instance_id.clone(),
199            Dom::from_ref(&bt_descriptor),
200        );
201        bt_descriptor
202    }
203
204    fn get_bluetooth_thread(&self) -> GenericSender<BluetoothRequest> {
205        self.global().as_window().bluetooth_thread()
206    }
207
208    // https://webbluetoothcg.github.io/web-bluetooth/#clean-up-the-disconnected-device
209    pub(crate) fn clean_up_disconnected_device(&self, cx: &mut js::context::JSContext) {
210        // Step 1.
211        self.get_gatt(cx).set_connected(false);
212
213        // TODO: Step 2: Implement activeAlgorithms internal slot for BluetoothRemoteGATTServer.
214
215        // Step 3: We don't need `context`, we get the attributeInstanceMap from the device.
216        // https://github.com/WebBluetoothCG/web-bluetooth/issues/330
217
218        // Step 4.
219        let mut service_map = self.attribute_instance_map.service_map.borrow_mut();
220        let service_ids = service_map.drain().map(|(id, _)| id).collect();
221
222        let mut characteristic_map = self.attribute_instance_map.characteristic_map.borrow_mut();
223        let characteristic_ids = characteristic_map.drain().map(|(id, _)| id).collect();
224
225        let mut descriptor_map = self.attribute_instance_map.descriptor_map.borrow_mut();
226        let descriptor_ids = descriptor_map.drain().map(|(id, _)| id).collect();
227
228        // Step 5, 6.4, 7.
229        // TODO: Step 6: Implement `active notification context set` for BluetoothRemoteGATTCharacteristic.
230        let _ = self
231            .get_bluetooth_thread()
232            .send(BluetoothRequest::SetRepresentedToNull(
233                service_ids,
234                characteristic_ids,
235                descriptor_ids,
236            ));
237
238        // Step 8.
239        self.upcast::<EventTarget>()
240            .fire_bubbling_event(atom!("gattserverdisconnected"), CanGc::from_cx(cx));
241    }
242
243    // https://webbluetoothcg.github.io/web-bluetooth/#garbage-collect-the-connection
244    pub(crate) fn garbage_collect_the_connection(
245        &self,
246        cx: &mut js::context::JSContext,
247    ) -> ErrorResult {
248        // Step 1: TODO: Check if other systems using this device.
249
250        // Step 2.
251        let context = self.get_context();
252        for (id, device) in context.get_device_map().borrow().iter() {
253            // Step 2.1 - 2.2.
254            if id == &self.Id().to_string() && device.get_gatt(cx).Connected() {
255                return Ok(());
256            }
257        }
258
259        // Step 3.
260        let (sender, receiver) =
261            generic_channel::channel(self.global().time_profiler_chan().clone()).unwrap();
262        self.get_bluetooth_thread()
263            .send(BluetoothRequest::GATTServerDisconnect(
264                String::from(self.Id()),
265                sender,
266            ))
267            .unwrap();
268        receiver.recv().unwrap().map_err(Convert::convert)
269    }
270}
271
272impl BluetoothDeviceMethods<crate::DomTypeHolder> for BluetoothDevice {
273    /// <https://webbluetoothcg.github.io/web-bluetooth/#dom-bluetoothdevice-id>
274    fn Id(&self) -> DOMString {
275        self.id.clone()
276    }
277
278    /// <https://webbluetoothcg.github.io/web-bluetooth/#dom-bluetoothdevice-name>
279    fn GetName(&self) -> Option<DOMString> {
280        self.name.clone()
281    }
282
283    /// <https://webbluetoothcg.github.io/web-bluetooth/#dom-bluetoothdevice-gatt>
284    fn GetGatt(
285        &self,
286        cx: &mut js::context::JSContext,
287    ) -> Option<DomRoot<BluetoothRemoteGATTServer>> {
288        // Step 1.
289        if self
290            .global()
291            .as_window()
292            .bluetooth_extra_permission_data()
293            .allowed_devices_contains_id(self.id.clone()) &&
294            !self.is_represented_device_null()
295        {
296            return Some(self.get_gatt(cx));
297        }
298        // Step 2.
299        None
300    }
301
302    /// <https://webbluetoothcg.github.io/web-bluetooth/#dom-bluetoothdevice-watchadvertisements>
303    fn WatchAdvertisements(&self, cx: &mut CurrentRealm) -> Rc<Promise> {
304        let p = Promise::new_in_realm(cx);
305        let sender = response_async(&p, self);
306        // TODO: Step 1.
307        // Note: Steps 2 - 3 are implemented in components/bluetooth/lib.rs in watch_advertisements function
308        // and in handle_response function.
309        self.get_bluetooth_thread()
310            .send(BluetoothRequest::WatchAdvertisements(
311                String::from(self.Id()),
312                sender,
313            ))
314            .unwrap();
315        p
316    }
317
318    /// <https://webbluetoothcg.github.io/web-bluetooth/#dom-bluetoothdevice-unwatchadvertisements>
319    fn UnwatchAdvertisements(&self) {
320        // Step 1.
321        self.watching_advertisements.set(false)
322        // TODO: Step 2.
323    }
324
325    /// <https://webbluetoothcg.github.io/web-bluetooth/#dom-bluetoothdevice-watchingadvertisements>
326    fn WatchingAdvertisements(&self) -> bool {
327        self.watching_advertisements.get()
328    }
329
330    // https://webbluetoothcg.github.io/web-bluetooth/#dom-bluetoothdeviceeventhandlers-ongattserverdisconnected
331    event_handler!(
332        gattserverdisconnected,
333        GetOngattserverdisconnected,
334        SetOngattserverdisconnected
335    );
336}
337
338impl AsyncBluetoothListener for BluetoothDevice {
339    fn handle_response(
340        &self,
341        cx: &mut js::context::JSContext,
342        response: BluetoothResponse,
343        promise: &Rc<Promise>,
344    ) {
345        match response {
346            // https://webbluetoothcg.github.io/web-bluetooth/#dom-bluetoothdevice-unwatchadvertisements
347            BluetoothResponse::WatchAdvertisements(_result) => {
348                // Step 3.1.
349                self.watching_advertisements.set(true);
350                // Step 3.2.
351                promise.resolve_native(&(), CanGc::from_cx(cx));
352            },
353            _ => promise.reject_error(
354                Error::Type(c"Something went wrong...".to_owned()),
355                CanGc::from_cx(cx),
356            ),
357        }
358    }
359}