Skip to main content

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