diff --git a/components/net/bluetooth_thread.rs b/components/net/bluetooth_thread.rs index ba7ab276ac0..a5bac3d5db4 100644 --- a/components/net/bluetooth_thread.rs +++ b/components/net/bluetooth_thread.rs @@ -8,6 +8,7 @@ use device::bluetooth::BluetoothGATTCharacteristic; use device::bluetooth::BluetoothGATTDescriptor; use device::bluetooth::BluetoothGATTService; use ipc_channel::ipc::{self, IpcReceiver, IpcSender}; +use net_traits::bluetooth_scanfilter::{RequestDeviceoptions, matches_filters}; use net_traits::bluetooth_thread::{BluetoothMethodMsg, BluetoothObjectMsg}; use std::borrow::ToOwned; use std::collections::HashMap; @@ -76,8 +77,8 @@ impl BluetoothManager { fn start(&mut self) { loop { match self.receiver.recv().unwrap() { - BluetoothMethodMsg::RequestDevice(sender) => { - self.request_device(sender) + BluetoothMethodMsg::RequestDevice(options, sender) => { + self.request_device(options, sender) } BluetoothMethodMsg::GATTServerConnect(device_id, sender) => { self.gatt_server_connect(device_id, sender) @@ -366,7 +367,9 @@ impl BluetoothManager { // Methods - fn request_device(&mut self, sender: IpcSender) { + fn request_device(&mut self, + options: RequestDeviceoptions, + sender: IpcSender) { let mut adapter = match self.get_adapter() { Some(a) => a, None => send_error!(sender, "No adapter found"), @@ -376,28 +379,30 @@ impl BluetoothManager { send_error!(sender, "No device found"); } - //TODO select the proper device - let device = &devices[0]; - - let message = BluetoothObjectMsg::BluetoothDevice { - id: device.get_address().unwrap_or("".to_owned()), - name: device.get_name().ok(), - device_class: device.get_class().ok(), - vendor_id_source: device.get_vendor_id_source().ok(), - vendor_id: device.get_vendor_id().ok(), - product_id: device.get_product_id().ok(), - product_version: device.get_device_id().ok(), - appearance: device.get_appearance().ok(), - tx_power: match device.get_tx_power() { - Ok(p) => Some(p as i8), - Err(_) => None, - }, - rssi: match device.get_rssi() { - Ok(p) => Some(p as i8), - Err(_) => None, - } - }; - sender.send(message).unwrap(); + match devices.into_iter().find(|ref d| matches_filters(d, options.get_filters())) { + Some(device) => { + let message = BluetoothObjectMsg::BluetoothDevice { + id: device.get_address().unwrap_or("".to_owned()), + name: device.get_name().ok(), + device_class: device.get_class().ok(), + vendor_id_source: device.get_vendor_id_source().ok(), + vendor_id: device.get_vendor_id().ok(), + product_id: device.get_product_id().ok(), + product_version: device.get_device_id().ok(), + appearance: device.get_appearance().ok(), + tx_power: match device.get_tx_power() { + Ok(p) => Some(p as i8), + Err(_) => None, + }, + rssi: match device.get_rssi() { + Ok(p) => Some(p as i8), + Err(_) => None, + }, + }; + sender.send(message).unwrap(); + }, + None => send_error!(sender, "No device found, that matches the given options"), + } } pub fn gatt_server_connect(&mut self, device_id: String, sender: IpcSender) { diff --git a/components/net_traits/Cargo.toml b/components/net_traits/Cargo.toml index 2f0158fd020..f53bb989a35 100644 --- a/components/net_traits/Cargo.toml +++ b/components/net_traits/Cargo.toml @@ -12,6 +12,7 @@ path = "lib.rs" util = {path = "../util"} msg = {path = "../msg"} ipc-channel = {git = "https://github.com/servo/ipc-channel"} +device = {git = "https://github.com/servo/devices"} heapsize = "0.3.0" heapsize_plugin = "0.1.2" hyper = { version = "0.9", features = [ "serde-serialization" ] } diff --git a/components/net_traits/bluetooth_scanfilter.rs b/components/net_traits/bluetooth_scanfilter.rs new file mode 100644 index 00000000000..58eb72f518e --- /dev/null +++ b/components/net_traits/bluetooth_scanfilter.rs @@ -0,0 +1,135 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use device::bluetooth::BluetoothDevice; + +// A device name can never be longer than 29 bytes. An adv packet is at most +// 31 bytes long. The length and identifier of the length field take 2 bytes. +// That least 29 bytes for the name. +const MAX_NAME_LENGTH: usize = 29; + +#[derive(Deserialize, Serialize)] +pub struct ServiceUUIDSequence(Vec); + +impl ServiceUUIDSequence { + pub fn new(vec: Vec) -> ServiceUUIDSequence { + ServiceUUIDSequence(vec) + } + + fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +#[derive(Deserialize, Serialize)] +pub struct BluetoothScanfilter { + name: String, + name_prefix: String, + services: ServiceUUIDSequence, +} + +impl BluetoothScanfilter { + fn is_empty_or_invalid_filter(&self) -> bool { + self.name.is_empty() && + self.name_prefix.is_empty() && + self.services.is_empty() && + self.name.len() > MAX_NAME_LENGTH && + self.name_prefix.len() > MAX_NAME_LENGTH + } + + pub fn new(name: String, name_prefix: String, services: Vec) -> BluetoothScanfilter { + BluetoothScanfilter { + name: name, + name_prefix: name_prefix, + services: ServiceUUIDSequence::new(services), + } + } +} + +#[derive(Deserialize, Serialize)] +pub struct BluetoothScanfilterSequence(Vec); + +impl BluetoothScanfilterSequence { + pub fn new(vec: Vec) -> BluetoothScanfilterSequence { + BluetoothScanfilterSequence(vec) + } +} + +impl BluetoothScanfilterSequence { + fn has_empty_or_invalid_filter(&self) -> bool { + self.0.is_empty() && + self.0.iter().all(|x| !(x.is_empty_or_invalid_filter())) + } +} + +#[derive(Deserialize, Serialize)] +pub struct RequestDeviceoptions { + filters: BluetoothScanfilterSequence, + optional_services: ServiceUUIDSequence, +} + +impl RequestDeviceoptions { + pub fn new(filters: BluetoothScanfilterSequence, + services: ServiceUUIDSequence) + -> RequestDeviceoptions { + RequestDeviceoptions { + filters: filters, + optional_services: services, + } + } + + pub fn get_filters(&self) -> &BluetoothScanfilterSequence { + &self.filters + } +} + +pub fn matches_filter(device: &BluetoothDevice, filter: &BluetoothScanfilter) -> bool { + if filter.is_empty_or_invalid_filter() { + return false; + } + + if !filter.name.is_empty() { + if let Ok(device_name) = device.get_name() { + if !device_name.eq(&filter.name) { + return false; + } + } else { + return false; + } + } + + if !filter.name_prefix.is_empty() { + if let Ok(device_name) = device.get_name() { + if !device_name.starts_with(&*filter.name_prefix) { + return false; + } + } else { + return false; + } + } + + if !filter.services.is_empty() { + if let Ok(stringvec) = device.get_uuids() { + for service in &filter.services.0 { + if !stringvec.iter().any(|x| x == service) { + return false; + } + } + } + } + return true; +} + +pub fn matches_filters(device: &BluetoothDevice, filters: &BluetoothScanfilterSequence) -> bool { + if filters.has_empty_or_invalid_filter() { + return false; + } + + for filter in &filters.0 { + if matches_filter(device, filter) { + return true; + } + } + return false; +} diff --git a/components/net_traits/bluetooth_thread.rs b/components/net_traits/bluetooth_thread.rs index c035a1ea164..6437027e144 100644 --- a/components/net_traits/bluetooth_thread.rs +++ b/components/net_traits/bluetooth_thread.rs @@ -1,11 +1,12 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +use bluetooth_scanfilter::RequestDeviceoptions; use ipc_channel::ipc::IpcSender; #[derive(Deserialize, Serialize)] pub enum BluetoothMethodMsg { - RequestDevice(IpcSender), + RequestDevice(RequestDeviceoptions, IpcSender), GATTServerConnect(String, IpcSender), GATTServerDisconnect(String, IpcSender), GetPrimaryService(String, String, IpcSender), diff --git a/components/net_traits/lib.rs b/components/net_traits/lib.rs index 45895f63711..17416d215dd 100644 --- a/components/net_traits/lib.rs +++ b/components/net_traits/lib.rs @@ -13,6 +13,7 @@ #![deny(unsafe_code)] +extern crate device; extern crate heapsize; extern crate hyper; extern crate image as piston_image; @@ -40,6 +41,7 @@ use std::thread; use url::Url; use websocket::header; +pub mod bluetooth_scanfilter; pub mod bluetooth_thread; pub mod hosts; pub mod image_cache_thread; diff --git a/components/script/dom/bluetooth.rs b/components/script/dom/bluetooth.rs index 36ac2a1f7e4..b66a427864a 100644 --- a/components/script/dom/bluetooth.rs +++ b/components/script/dom/bluetooth.rs @@ -2,18 +2,38 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +use core::clone::Clone; use dom::bindings::codegen::Bindings::BluetoothBinding; -use dom::bindings::codegen::Bindings::BluetoothBinding::BluetoothMethods; +use dom::bindings::codegen::Bindings::BluetoothBinding::RequestDeviceOptions; +use dom::bindings::codegen::Bindings::BluetoothBinding::{BluetoothScanFilter, BluetoothMethods}; use dom::bindings::codegen::Bindings::BluetoothDeviceBinding::VendorIDSource; +use dom::bindings::error::Error::Type; +use dom::bindings::error::Fallible; use dom::bindings::global::GlobalRef; use dom::bindings::js::Root; use dom::bindings::reflector::{Reflectable, Reflector, reflect_dom_object}; use dom::bluetoothadvertisingdata::BluetoothAdvertisingData; use dom::bluetoothdevice::BluetoothDevice; +use dom::bluetoothuuid::BluetoothUUID; use ipc_channel::ipc::{self, IpcSender}; +use net_traits::bluetooth_scanfilter::{BluetoothScanfilter, BluetoothScanfilterSequence}; +use net_traits::bluetooth_scanfilter::{RequestDeviceoptions, ServiceUUIDSequence}; use net_traits::bluetooth_thread::{BluetoothMethodMsg, BluetoothObjectMsg}; use util::str::DOMString; +// A device name can never be longer than 29 bytes. An adv packet is at most +// 31 bytes long. The length and identifier of the length field take 2 bytes. +// That least 29 bytes for the name. +const MAX_FILTER_NAME_LENGTH: usize = 29; +// 248 is the maximum number of UTF-8 code units in a Bluetooth Device Name. +const MAX_DEVICE_NAME_LENGTH: usize = 248; +const FILTER_EMPTY_ERROR: &'static str = "'filters' member must be non - empty to find any devices."; +const FILTER_ERROR: &'static str = "A filter must restrict the devices in some way."; +const FILTER_NAME_TOO_LONG_ERROR: &'static str = "A 'name' or 'namePrefix' can't be longer then 29 bytes."; +const NAME_PREFIX_ERROR: &'static str = "'namePrefix', if present, must be non - empty."; +const NAME_TOO_LONG_ERROR: &'static str = "A device name can't be longer than 248 bytes."; +const SERVICE_ERROR: &'static str = "'services', if present, must contain at least one service."; + // https://webbluetoothcg.github.io/web-bluetooth/#bluetooth #[dom_struct] pub struct Bluetooth { @@ -40,59 +60,162 @@ impl Bluetooth { } } -impl BluetoothMethods for Bluetooth { - - // https://webbluetoothcg.github.io/web-bluetooth/#dom-bluetooth-requestdevice - fn RequestDevice(&self) -> Option> { - let (sender, receiver) = ipc::channel().unwrap(); - self.get_bluetooth_thread().send(BluetoothMethodMsg::RequestDevice(sender)).unwrap(); - let device = receiver.recv().unwrap(); - match device { - BluetoothObjectMsg::BluetoothDevice { - id, - name, - device_class, - vendor_id_source, - vendor_id, - product_id, - product_version, - appearance, - tx_power, - rssi, - } => { - let ad_data = &BluetoothAdvertisingData::new(self.global().r(), - appearance, - tx_power, - rssi); - let vendor_id_source = match vendor_id_source { - Some(vid) => match vid.as_ref() { - "bluetooth" => Some(VendorIDSource::Bluetooth), - "usb" => Some(VendorIDSource::Usb), - _ => Some(VendorIDSource::Unknown), - }, - None => None, - }; - let name = match name { - Some(n) => Some(DOMString::from(n)), - None => None, - }; - Some(BluetoothDevice::new(self.global().r(), - DOMString::from(id), - name, - ad_data, - device_class, - vendor_id_source, - vendor_id, - product_id, - product_version)) - }, - BluetoothObjectMsg::Error { - error - } => { - println!("{}", error); - None - }, - _ => unreachable!() +impl Clone for BluetoothScanFilter { + fn clone(&self) -> BluetoothScanFilter { + BluetoothScanFilter { + name: self.name.clone(), + namePrefix: self.namePrefix.clone(), + services: self.services.clone(), + } + } +} + +impl Clone for RequestDeviceOptions { + fn clone(&self) -> RequestDeviceOptions { + RequestDeviceOptions { + filters: self.filters.clone(), + optionalServices: self.optionalServices.clone(), + } + } +} + +fn canonicalize_filter(filter: &BluetoothScanFilter, global: GlobalRef) + -> Fallible { + if !(filter.services.is_some() || + filter.name.is_some() || + filter.namePrefix.is_some()) { + return Err(Type(FILTER_ERROR.to_owned())); + } + + let mut services_vec: Vec = vec!(); + if let Some(services) = filter.services.clone() { + if services.is_empty() { + return Err(Type(SERVICE_ERROR.to_owned())); + } + for service in services { + match BluetoothUUID::GetService(global, service) { + Ok(valid) => services_vec.push(valid.to_string()), + Err(err) => return Err(err), + } + } + } + + let mut name = String::new(); + if let Some(filter_name) = filter.name.clone() { + //NOTE: DOMString::len() gives back the size in bytes + if filter_name.len() > MAX_DEVICE_NAME_LENGTH { + return Err(Type(NAME_TOO_LONG_ERROR.to_owned())); + } + if filter_name.len() > MAX_FILTER_NAME_LENGTH { + return Err(Type(FILTER_NAME_TOO_LONG_ERROR.to_owned())); + } + name = filter_name.to_string(); + } + + let mut name_prefix = String::new(); + if let Some(filter_name_prefix) = filter.namePrefix.clone() { + if filter_name_prefix.len() == 0 { + return Err(Type(NAME_PREFIX_ERROR.to_owned())); + } + if filter_name_prefix.len() > MAX_DEVICE_NAME_LENGTH { + return Err(Type(NAME_TOO_LONG_ERROR.to_owned())); + } + if filter_name_prefix.len() > MAX_FILTER_NAME_LENGTH { + return Err(Type(FILTER_NAME_TOO_LONG_ERROR.to_owned())); + } + name_prefix = filter_name_prefix.to_string(); + } + + Ok(BluetoothScanfilter::new(name, name_prefix, services_vec)) +} + +fn convert_request_device_options(options: &RequestDeviceOptions, global: GlobalRef) + -> Fallible { + if options.filters.is_empty() { + return Err(Type(FILTER_EMPTY_ERROR.to_owned())); + } + + let mut filters = vec!(); + for filter in &options.filters { + match canonicalize_filter(&filter, global) { + Ok(canonicalized_filter) => filters.push(canonicalized_filter), + Err(err) => return Err(err), + } + } + + let mut optional_services = vec!(); + if let Some(opt_services) = options.optionalServices.clone() { + for opt_service in opt_services { + match BluetoothUUID::GetService(global, opt_service) { + Ok(valid_service) => optional_services.push(valid_service.to_string()), + Err(err) => return Err(err), + } + } + } + + Ok(RequestDeviceoptions::new(BluetoothScanfilterSequence::new(filters), + ServiceUUIDSequence::new(optional_services))) +} + +impl BluetoothMethods for Bluetooth { + + // https://webbluetoothcg.github.io/web-bluetooth/#dom-bluetooth-requestdevice + fn RequestDevice(&self, + option: &RequestDeviceOptions) + -> Fallible> { + let (sender, receiver) = ipc::channel().unwrap(); + match convert_request_device_options(option, self.global().r()) { + Ok(option) => { + self.get_bluetooth_thread() + .send(BluetoothMethodMsg::RequestDevice(option, sender)) + .unwrap(); + let device = receiver.recv().unwrap(); + match device { + BluetoothObjectMsg::BluetoothDevice { + id, + name, + device_class, + vendor_id_source, + vendor_id, + product_id, + product_version, + appearance, + tx_power, + rssi, + } => { + let ad_data = &BluetoothAdvertisingData::new(self.global().r(), + appearance, + tx_power, + rssi); + let vendor_id_source = match vendor_id_source { + Some(vid) => match vid.as_ref() { + "bluetooth" => Some(VendorIDSource::Bluetooth), + "usb" => Some(VendorIDSource::Usb), + _ => Some(VendorIDSource::Unknown), + }, + None => None, + }; + let name = match name { + Some(n) => Some(DOMString::from(n)), + None => None, + }; + Ok(BluetoothDevice::new(self.global().r(), + DOMString::from(id), + name, + ad_data, + device_class, + vendor_id_source, + vendor_id, + product_id, + product_version)) + }, + BluetoothObjectMsg::Error { + error + } => return Err(Type(error)), + _ => unreachable!() + } + }, + Err(err) => Err(err), } } } diff --git a/components/script/dom/webidls/Bluetooth.webidl b/components/script/dom/webidls/Bluetooth.webidl index 4199e0bc486..0d6c381e65f 100644 --- a/components/script/dom/webidls/Bluetooth.webidl +++ b/components/script/dom/webidls/Bluetooth.webidl @@ -4,10 +4,22 @@ // https://webbluetoothcg.github.io/web-bluetooth/#bluetooth +dictionary BluetoothScanFilter { + sequence services; + DOMString name; + DOMString namePrefix; +}; + +dictionary RequestDeviceOptions { + required sequence filters; + sequence optionalServices /*= []*/; +}; + [Pref="dom.bluetooth.enabled"] interface Bluetooth { // Promise requestDevice(RequestDeviceOptions options); - BluetoothDevice? requestDevice(/*RequestDeviceOptions options*/); + [Throws] + BluetoothDevice requestDevice(RequestDeviceOptions options); }; // Bluetooth implements EventTarget; diff --git a/components/servo/Cargo.lock b/components/servo/Cargo.lock index a3a64440eb3..97bb0b56d8a 100644 --- a/components/servo/Cargo.lock +++ b/components/servo/Cargo.lock @@ -1394,6 +1394,7 @@ dependencies = [ name = "net_traits" version = "0.0.1" dependencies = [ + "device 0.0.1 (git+https://github.com/dati91/devices?branch=device-api)", "heapsize 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "heapsize_plugin 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", "hyper 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",