Skip to content

Commit

Permalink
Bluetooth Permission API integration
Browse files Browse the repository at this point in the history
  • Loading branch information
Zakor Gyula authored and dati91 committed Feb 13, 2017
1 parent f3ddee5 commit 5287cd3
Show file tree
Hide file tree
Showing 17 changed files with 550 additions and 45 deletions.
1 change: 1 addition & 0 deletions components/atoms/static_atoms.txt
Expand Up @@ -65,3 +65,4 @@ characteristicvaluechanged
fullscreenchange
fullscreenerror
gattserverdisconnected
onchange
23 changes: 18 additions & 5 deletions components/bluetooth/lib.rs
Expand Up @@ -256,6 +256,9 @@ impl BluetoothManager {
BluetoothRequest::GetAvailability(sender) => {
let _ = sender.send(self.get_availability());
},
BluetoothRequest::MatchesFilter(id, filters, sender) => {
let _ = sender.send(self.device_matches_filter(&id, &filters));
},
BluetoothRequest::Exit => {
break
},
Expand Down Expand Up @@ -425,6 +428,17 @@ impl BluetoothManager {
self.cached_devices.contains_key(device_id) && self.address_to_id.values().any(|v| v == device_id)
}

fn device_matches_filter(&mut self,
device_id: &str,
filters: &BluetoothScanfilterSequence)
-> BluetoothResult<bool> {
let mut adapter = try!(self.get_adapter());
match self.get_device(&mut adapter, device_id) {
Some(ref device) => Ok(matches_filters(device, filters)),
None => Ok(false),
}
}

// Service

fn get_and_cache_gatt_services(&mut self,
Expand Down Expand Up @@ -561,6 +575,9 @@ impl BluetoothManager {
-> BluetoothResponseResult {
// Step 6.
let mut adapter = try!(self.get_adapter());

// Step 7.
// Note: There are no requiredServiceUUIDS, we scan for all devices.
if let Ok(ref session) = adapter.create_discovery_session() {
if session.start_discovery().is_ok() {
if !is_mock_adapter(&adapter) {
Expand All @@ -570,8 +587,6 @@ impl BluetoothManager {
let _ = session.stop_discovery();
}

// Step 7.
// Note: There are no requiredServiceUUIDS, we scan for all devices.
let mut matched_devices = self.get_and_cache_devices(&mut adapter);

// Step 8.
Expand All @@ -582,8 +597,6 @@ impl BluetoothManager {
}

// Step 9.
// TODO: After the permission API implementation
// https://w3c.github.io/permissions/#prompt-the-user-to-choose
if let Some(address) = self.select_device(matched_devices, &adapter) {
let device_id = match self.address_to_id.get(&address) {
Some(id) => id.clone(),
Expand All @@ -602,7 +615,7 @@ impl BluetoothManager {
return Ok(BluetoothResponse::RequestDevice(message));
}
}
// TODO: Step 10 - 11: Implement the permission API.
// Step 10.
return Err(BluetoothError::NotFound);
// Step 12: Missing, because it is optional.
}
Expand Down
3 changes: 2 additions & 1 deletion components/bluetooth_traits/lib.rs
Expand Up @@ -12,7 +12,7 @@ pub mod blocklist;
pub mod scanfilter;

use ipc_channel::ipc::IpcSender;
use scanfilter::RequestDeviceoptions;
use scanfilter::{BluetoothScanfilterSequence, RequestDeviceoptions};

#[derive(Deserialize, Serialize)]
pub enum BluetoothError {
Expand Down Expand Up @@ -92,6 +92,7 @@ pub enum BluetoothRequest {
SetRepresentedToNull(Vec<String>, Vec<String>, Vec<String>),
IsRepresentedDeviceNull(String, IpcSender<bool>),
GetAvailability(IpcSender<BluetoothResponseResult>),
MatchesFilter(String, BluetoothScanfilterSequence, IpcSender<BluetoothResult<bool>>),
Test(String, IpcSender<BluetoothResult<()>>),
Exit,
}
Expand Down
229 changes: 221 additions & 8 deletions components/script/dom/bluetooth.rs
Expand Up @@ -11,23 +11,34 @@ use core::clone::Clone;
use dom::bindings::cell::DOMRefCell;
use dom::bindings::codegen::Bindings::BluetoothBinding::{self, BluetoothDataFilterInit, BluetoothLEScanFilterInit};
use dom::bindings::codegen::Bindings::BluetoothBinding::{BluetoothMethods, RequestDeviceOptions};
use dom::bindings::codegen::Bindings::BluetoothPermissionResultBinding::AllowedBluetoothDevice;
use dom::bindings::codegen::Bindings::BluetoothPermissionResultBinding::BluetoothPermissionData;
use dom::bindings::codegen::Bindings::BluetoothPermissionResultBinding::BluetoothPermissionDescriptor;
use dom::bindings::codegen::Bindings::BluetoothRemoteGATTServerBinding::BluetoothRemoteGATTServerBinding::
BluetoothRemoteGATTServerMethods;
use dom::bindings::codegen::Bindings::EventHandlerBinding::EventHandlerNonNull;
use dom::bindings::codegen::UnionTypes::StringOrUnsignedLong;
use dom::bindings::codegen::Bindings::PermissionStatusBinding::PermissionState;
use dom::bindings::codegen::UnionTypes::{StringOrStringSequence, StringOrUnsignedLong};
use dom::bindings::error::Error::{self, Network, Security, Type};
use dom::bindings::error::Fallible;
use dom::bindings::js::{JS, Root};
use dom::bindings::refcounted::{Trusted, TrustedPromise};
use dom::bindings::reflector::{DomObject, reflect_dom_object};
use dom::bindings::str::DOMString;
use dom::bluetoothdevice::BluetoothDevice;
use dom::bluetoothpermissionresult::BluetoothPermissionResult;
use dom::bluetoothuuid::{BluetoothServiceUUID, BluetoothUUID, UUID};
use dom::eventtarget::EventTarget;
use dom::globalscope::GlobalScope;
use dom::permissions::{get_descriptor_permission_state, PermissionAlgorithm};
use dom::promise::Promise;
use ipc_channel::ipc::{self, IpcSender};
use ipc_channel::router::ROUTER;
use js::jsapi::{JSAutoCompartment, JSContext};
use js::conversions::ConversionResult;
use js::jsapi::{JSAutoCompartment, JSContext, JSObject};
use js::jsval::{ObjectValue, UndefinedValue};
use script_thread::Runnable;
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::str::FromStr;
Expand All @@ -46,6 +57,48 @@ const SERVICE_DATA_ERROR: &'static str = "'serviceData', if present, must be non
const SERVICE_ERROR: &'static str = "'services', if present, must contain at least one service.";
const OPTIONS_ERROR: &'static str = "Fields of 'options' conflict with each other.
Either 'acceptAllDevices' member must be true, or 'filters' member must be set to a value.";
const BT_DESC_CONVERSION_ERROR: &'static str = "Can't convert to an IDL value of type BluetoothPermissionDescriptor";


thread_local!(pub static EXTRA_PERMISSION_DATA: RefCell<BluetoothPermissionData> =
RefCell::new(BluetoothPermissionData { allowedDevices: Vec::new() }));

pub fn add_new_allowed_device(allowed_device: AllowedBluetoothDevice) {
EXTRA_PERMISSION_DATA.with(|epdata| {
epdata.borrow_mut().allowedDevices.push(allowed_device);
});
}

fn get_allowed_devices() -> Vec<AllowedBluetoothDevice> {
EXTRA_PERMISSION_DATA.with(|epdata| {
epdata.borrow().allowedDevices.clone()
})
}

pub fn allowed_devices_contains_id(id: DOMString) -> bool {
EXTRA_PERMISSION_DATA.with(|epdata| {
epdata.borrow_mut().allowedDevices.iter().any(|d| d.deviceId == id)
})
}

impl Clone for StringOrStringSequence {
fn clone(&self) -> StringOrStringSequence {
match self {
&StringOrStringSequence::String(ref s) => StringOrStringSequence::String(s.clone()),
&StringOrStringSequence::StringSequence(ref v) => StringOrStringSequence::StringSequence(v.clone()),
}
}
}

impl Clone for AllowedBluetoothDevice {
fn clone(&self) -> AllowedBluetoothDevice {
AllowedBluetoothDevice {
deviceId: self.deviceId.clone(),
mayUseGATT: self.mayUseGATT,
allowedServices: self.allowedServices.clone(),
}
}
}

struct BluetoothContext<T: AsyncBluetoothListener + DomObject> {
promise: Option<TrustedPromise>,
Expand Down Expand Up @@ -107,7 +160,8 @@ impl Bluetooth {
fn request_bluetooth_devices(&self,
p: &Rc<Promise>,
filters: &Option<Vec<BluetoothLEScanFilterInit>>,
optional_services: &Option<Vec<BluetoothServiceUUID>>) {
optional_services: &Option<Vec<BluetoothServiceUUID>>,
sender: IpcSender<BluetoothResponseResult>) {
// TODO: Step 1: Triggered by user activation.

// Step 2.2: There are no requiredServiceUUIDS, we scan for all devices.
Expand Down Expand Up @@ -161,11 +215,15 @@ impl Bluetooth {
let option = RequestDeviceoptions::new(BluetoothScanfilterSequence::new(uuid_filters),
ServiceUUIDSequence::new(optional_services_uuids));

// TODO: Step 3 - 5: Implement the permission API.
// Step 3 - 5
// FIXME The following call will create a popup, which will mess up the testing...
// Maybe create a call to the lower level to check if we are testing or not
// if let PermissionState::Denied = get_descriptor_permission_state(PermissionName::Bluetooth, None) {
// return p.reject_error(p.global().get_cx(), Error::NotFound);
// }

// Note: Steps 6 - 8 are implemented in
// components/net/bluetooth_thread.rs in request_device function.
let sender = response_async(p, self);
self.get_bluetooth_thread().send(BluetoothRequest::RequestDevice(option, sender)).unwrap();
}
}
Expand Down Expand Up @@ -438,7 +496,8 @@ impl BluetoothMethods for Bluetooth {
}

// Step 2.
self.request_bluetooth_devices(&p, &option.filters, &option.optionalServices);
let sender = response_async(&p, self);
self.request_bluetooth_devices(&p, &option.filters, &option.optionalServices, sender);
//Note: Step 3 - 4. in response function, Step 5. in handle_response function.
return p;
}
Expand All @@ -463,7 +522,7 @@ impl AsyncBluetoothListener for Bluetooth {
fn handle_response(&self, response: BluetoothResponse, promise_cx: *mut JSContext, promise: &Rc<Promise>) {
match response {
// https://webbluetoothcg.github.io/web-bluetooth/#request-bluetooth-devices
// Step 13 - 14.
// Step 11, 13 - 14.
BluetoothResponse::RequestDevice(device) => {
let mut device_instance_map = self.device_instance_map.borrow_mut();
if let Some(existing_device) = device_instance_map.get(&device.id.clone()) {
Expand All @@ -473,7 +532,16 @@ impl AsyncBluetoothListener for Bluetooth {
DOMString::from(device.id.clone()),
device.name.map(DOMString::from),
&self);
device_instance_map.insert(device.id, JS::from_ref(&bt_device));
device_instance_map.insert(device.id.clone(), JS::from_ref(&bt_device));
add_new_allowed_device(
AllowedBluetoothDevice {
// TODO fix this
// allowedServices only relevant if the device store it as an inter slot as well
allowedServices: StringOrStringSequence::String(DOMString::from("all".to_owned())),
deviceId: DOMString::from(device.id),
mayUseGATT: true,
}
);
// https://webbluetoothcg.github.io/web-bluetooth/#dom-bluetooth-requestdevice
// Step 5.
promise.resolve_native(promise_cx, &bt_device);
Expand All @@ -487,3 +555,148 @@ impl AsyncBluetoothListener for Bluetooth {
}
}
}

impl PermissionAlgorithm for Bluetooth {
type Descriptor = BluetoothPermissionDescriptor;
type Status = BluetoothPermissionResult;

#[allow(unsafe_code)]
fn create_descriptor(cx: *mut JSContext,
permission_descriptor_obj: *mut JSObject)
-> Result<BluetoothPermissionDescriptor, Error> {
rooted!(in(cx) let mut property = UndefinedValue());
property.handle_mut().set(ObjectValue(permission_descriptor_obj));
unsafe {
match BluetoothPermissionDescriptor::new(cx, property.handle()) {
Ok(ConversionResult::Success(descriptor)) => Ok(descriptor),
Ok(ConversionResult::Failure(error)) => Err(Error::Type(error.into_owned())),
Err(_) => Err(Error::Type(String::from(BT_DESC_CONVERSION_ERROR))),
}
}
}

#[allow(unrooted_must_root)]
// https://webbluetoothcg.github.io/web-bluetooth/#query-the-bluetooth-permission
fn permission_query(cx: *mut JSContext, promise: &Rc<Promise>,
descriptor: &BluetoothPermissionDescriptor,
status: &BluetoothPermissionResult) {
// Step 1.
// TODO: `environment settings object` is not implemented in Servo yet.

// Step 2.
status.set_state(get_descriptor_permission_state(status.get_query(), None));

// Step 3.
if let PermissionState::Denied = status.get_state() {
status.set_devices(Vec::new());
return promise.resolve_native(cx, status);
}

// Step 4.
let mut matching_devices: Vec<JS<BluetoothDevice>> = Vec::new();

// TODO: Step 5: Create a map between the current setting object and BluetoothPermissionData
// extra permission data, which replaces the exisitng EXTRA_PERMISSION_DATA global variable.
// For this also use the extra permission data constraints from the specification:
// https://webbluetoothcg.github.io/web-bluetooth/#dictdef-bluetoothpermissiondata

// Step 5.
let allowed_devices = get_allowed_devices();

let bluetooth = status.get_bluetooth();
let device_map = bluetooth.get_device_map().borrow();

// Step 6.
for allowed_device in allowed_devices {
// Step 6.1.
if let Some(ref id) = descriptor.deviceId {
if &allowed_device.deviceId != id {
continue;
}
}
let device_id = String::from(allowed_device.deviceId.as_ref());
if let Some(ref filters) = descriptor.filters {
let mut scan_filters: Vec<BluetoothScanfilter> = Vec::new();

// NOTE(zakorgy): This canonicalizing step is missing from the specification.
// But there is an issue for this: https://github.com/WebBluetoothCG/web-bluetooth/issues/347
for filter in filters {
match canonicalize_filter(&filter) {
Ok(f) => scan_filters.push(f),
Err(error) => return promise.reject_error(cx, error),
}
}

// Step 6.2.
// Instead of creating an internal slot we send an ipc message to the Bluetooth thread
// to check if one of the filters matches.
let (sender, receiver) = ipc::channel().unwrap();
status.get_bluetooth_thread()
.send(BluetoothRequest::MatchesFilter(device_id.clone(),
BluetoothScanfilterSequence::new(scan_filters),
sender)).unwrap();

match receiver.recv().unwrap() {
Ok(true) => (),
Ok(false) => continue,
Err(error) => return promise.reject_error(cx, Error::from(error)),
};
}

// Step 6.4.
// TODO: Implement this correctly, not just using device ids here.
// https://webbluetoothcg.github.io/web-bluetooth/#get-the-bluetoothdevice-representing
if let Some(ref device) = device_map.get(&device_id) {
matching_devices.push(JS::from_ref(&*device));
}
}

// Step 7.
status.set_devices(matching_devices);

// https://w3c.github.io/permissions/#dom-permissions-query
// Step 7.
promise.resolve_native(cx, status);
}

// NOTE(zakorgy): There is no link for this algorithm until this PR for the spec is pending:
// https://github.com/WebBluetoothCG/web-bluetooth/pull/349
fn permission_request(cx: *mut JSContext, promise: &Rc<Promise>,
descriptor: &BluetoothPermissionDescriptor,
status: &BluetoothPermissionResult) {
// Step 1.
if descriptor.filters.is_some() == descriptor.acceptAllDevices {
return promise.reject_error(cx, Error::Type(OPTIONS_ERROR.to_owned()));
}

// Step 2.
let sender = response_async(promise, status);
let bluetooth = status.get_bluetooth();
bluetooth.request_bluetooth_devices(promise, &descriptor.filters, &descriptor.optionalServices, sender);

// NOTE: Step 3. is in BluetoothPermissionResult's `handle_response` function.
}

#[allow(unrooted_must_root)]
// https://webbluetoothcg.github.io/web-bluetooth/#revoke-bluetooth-access
fn permission_revoke(_descriptor: &BluetoothPermissionDescriptor, status: &BluetoothPermissionResult) {
// Step 1.
let allowed_devices = get_allowed_devices();
// Step 2.
let bluetooth = status.get_bluetooth();
let device_map = bluetooth.get_device_map().borrow();
for (id, device) in device_map.iter() {
let id = DOMString::from(id.clone());
// Step 2.1.
if allowed_devices.iter().any(|d| d.deviceId == id) &&
!device.is_represented_device_null() {
// Note: We don't need to update the allowed_services,
// because we store it in the lower level
// where it is already up-to-date
continue;
}
// Step 2.2 - 2.4
let _ = device.get_gatt().Disconnect();
}
}
}

0 comments on commit 5287cd3

Please sign in to comment.