blob: 13699461a7189d0dc2606af12c195123db653657 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use anyhow::{format_err, Error};
use fidl::endpoints::{create_request_stream, Responder};
use fidl_fuchsia_bluetooth_gatt2 as gatt;
use fuchsia_bluetooth::types::Uuid;
use futures::{channel::mpsc, SinkExt, StreamExt};
use tracing::{info, warn};
use crate::host_dispatcher::HostDispatcher;
const GENERIC_ACCESS_SERVICE_UUID: Uuid = Uuid::new16(0x1800);
const GENERIC_ACCESS_DEVICE_NAME_UUID: Uuid = Uuid::new16(0x2A00);
const GENERIC_ACCESS_APPEARANCE_UUID: Uuid = Uuid::new16(0x2A01);
const GENERIC_ACCESS_DEVICE_NAME_ID: u64 = 0x2A00;
const GENERIC_ACCESS_APPEARANCE_ID: u64 = 0x2A01;
fn build_generic_access_service_info() -> gatt::ServiceInfo {
// The spec says these characteristics should be readable, but optionally writeable. For
// simplicity, we've disallowed them being peer-writeable. We enable access to these
// characteristics with no security, as they are sent out freely during advertising anyway.
let device_name_characteristic = gatt::Characteristic {
handle: Some(gatt::Handle { value: GENERIC_ACCESS_DEVICE_NAME_ID }),
type_: Some(GENERIC_ACCESS_DEVICE_NAME_UUID.into()),
properties: Some(gatt::CharacteristicPropertyBits::READ),
permissions: Some(gatt::AttributePermissions {
read: Some(gatt::SecurityRequirements::default()),
..Default::default()
}),
..Default::default()
};
let appearance_characteristic = gatt::Characteristic {
handle: Some(gatt::Handle { value: GENERIC_ACCESS_APPEARANCE_ID }),
type_: Some(GENERIC_ACCESS_APPEARANCE_UUID.into()),
properties: Some(gatt::CharacteristicPropertyBits::READ),
permissions: Some(gatt::AttributePermissions {
read: Some(gatt::SecurityRequirements::default()),
..Default::default()
}),
..Default::default()
};
gatt::ServiceInfo {
// This value is ignored as this is a local-only service
handle: Some(gatt::ServiceHandle { value: 0 }),
// Secondary services are only rarely used and this is not one of those cases
kind: Some(gatt::ServiceKind::Primary),
type_: Some(GENERIC_ACCESS_SERVICE_UUID.into()),
characteristics: Some(vec![device_name_characteristic, appearance_characteristic]),
..Default::default()
}
}
/// A GasProxy forwards peer Generic Access Service requests received by a BT host to the local GAS
/// task. A GasProxy will be spawned as a task by HostDispatcher whenever a new host is detected.
/// Passing the requests through proxies is preferable to the task maintaining host state so that
/// we can limit host state to one place, HostDispatcher. This will simplify supporting multiple
/// Bluetooth hosts from within a single HostDispatcher in the future.
pub struct GasProxy {
service_request_stream: gatt::LocalServiceRequestStream,
gas_task_channel: mpsc::Sender<gatt::LocalServiceRequest>,
// We have to hold on to these connections to the Hosts GATT server even though we never use them because
// otherwise the host will shut down the connection to the Generic Access Server.
_gatt_server: gatt::Server_Proxy,
}
impl GasProxy {
pub async fn new(
gatt_server: gatt::Server_Proxy,
gas_task_channel: mpsc::Sender<gatt::LocalServiceRequest>,
) -> Result<GasProxy, Error> {
let (service_client, service_request_stream) =
create_request_stream::<gatt::LocalServiceMarker>()?;
let service_info = build_generic_access_service_info();
gatt_server.publish_service(&service_info, service_client).await?.map_err(|e| {
format_err!("Failed to publish Generic Access Service to GATT server: {:?}", e)
})?;
info!("Published Generic Access Service to local device database.");
Ok(GasProxy { service_request_stream, gas_task_channel, _gatt_server: gatt_server })
}
pub async fn run(mut self) -> Result<(), Error> {
while let Some(req) = self.service_request_stream.next().await {
if let Err(send_err) = self.gas_task_channel.send(req?).await {
if send_err.is_disconnected() {
return Ok(());
}
return Err(send_err.into());
}
}
Ok(())
}
}
/// Struct holding the state needed to run the Generic Access Service task, which
/// serves requests to the Generic Access Service from other devices per the BT spec.
/// To avoid shared state it reads back into HostDispatcher to see the values of the
/// service characteristics (name/appearance). The stream of requests is abstracted
/// from being tied to a specific host - HostDispatcher is set up so that when any
/// new host is set up, it ties the sender end of that channel to an instance of
/// the GAS Proxy task, which proxies the requests from that specific host to the
/// sender end of the channel stored in this struct.
pub struct GenericAccessService {
hd: HostDispatcher,
generic_access_req_stream: mpsc::Receiver<gatt::LocalServiceRequest>,
}
impl GenericAccessService {
pub fn build(
hd: &HostDispatcher,
request_stream: mpsc::Receiver<gatt::LocalServiceRequest>,
) -> Self {
Self { hd: hd.clone(), generic_access_req_stream: request_stream }
}
fn send_read_response(
&self,
responder: gatt::LocalServiceReadValueResponder,
id: u64,
) -> Result<(), fidl::Error> {
match id {
GENERIC_ACCESS_DEVICE_NAME_ID => responder.send(Ok(self.hd.get_name().as_bytes())),
GENERIC_ACCESS_APPEARANCE_ID => {
responder.send(Ok(&self.hd.get_appearance().into_primitive().to_le_bytes()))
}
_ => responder.send(Err(gatt::Error::ReadNotPermitted)),
}
}
fn process_service_req(&self, request: gatt::LocalServiceRequest) -> Result<(), Error> {
match request {
gatt::LocalServiceRequest::ReadValue { responder, handle, .. } => {
Ok(self.send_read_response(responder, handle.value)?)
}
// Writing to the the available GENERIC_ACCESS service characteristics
// is optional according to the spec, and it was decided not to implement
gatt::LocalServiceRequest::WriteValue { responder, .. } => {
Ok(responder.send(Err(gatt::Error::WriteNotPermitted))?)
}
gatt::LocalServiceRequest::PeerUpdate { payload: _, responder } => {
Ok(responder.drop_without_shutdown())
}
// Ignore CharacteristicConfiguration, ValueChangedCredit, etc.
_ => Ok(()),
}
}
pub async fn run(mut self) {
while let Some(request) = self.generic_access_req_stream.next().await {
self.process_service_req(request).unwrap_or_else(|e| {
warn!("Error handling Generic Access Service Request: {:?}", e);
});
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use async_helpers::hanging_get::asynchronous as hanging_get;
use fidl::endpoints::create_proxy_and_stream;
use fidl_fuchsia_bluetooth::{Appearance, PeerId};
use fuchsia_async as fasync;
use fuchsia_inspect as inspect;
use futures::FutureExt;
use std::collections::HashMap;
use crate::host_dispatcher::{NameReplace, DEFAULT_DEVICE_NAME};
use crate::store::stash::Stash;
const TEST_DEVICE_APPEARANCE: Appearance = Appearance::Computer;
// Starts tasks that run the GasProxy and GenericAccessService and returns the associated
// LocalServiceProxy and HostDispatcher.
fn setup_generic_access_service() -> (gatt::LocalServiceProxy, HostDispatcher) {
let (service_client, service_request_stream) =
create_proxy_and_stream::<gatt::LocalServiceMarker>().unwrap();
let (gas_task_channel, generic_access_req_stream) =
mpsc::channel::<gatt::LocalServiceRequest>(0);
let (gatt_server, _gatt_server_remote) =
create_proxy_and_stream::<gatt::Server_Marker>().unwrap();
let gas_proxy = GasProxy {
service_request_stream,
gas_task_channel: gas_task_channel.clone(),
_gatt_server: gatt_server,
};
fasync::Task::spawn(gas_proxy.run().map(|r| {
r.unwrap_or_else(|err| {
warn!("Error running Generic Access proxy in task: {:?}", err);
})
}))
.detach();
let stash = Stash::in_memory_mock();
let inspector = inspect::Inspector::default();
let system_inspect = inspector.root().create_child("system");
let watch_peers_broker = hanging_get::HangingGetBroker::new(
HashMap::new(),
|_, _| true,
hanging_get::DEFAULT_CHANNEL_SIZE,
);
let watch_hosts_broker = hanging_get::HangingGetBroker::new(
Vec::new(),
|_, _| true,
hanging_get::DEFAULT_CHANNEL_SIZE,
);
let dispatcher = HostDispatcher::new(
TEST_DEVICE_APPEARANCE,
stash,
system_inspect,
gas_task_channel,
watch_peers_broker.new_publisher(),
watch_peers_broker.new_registrar(),
watch_hosts_broker.new_publisher(),
watch_hosts_broker.new_registrar(),
);
let service = GenericAccessService { hd: dispatcher.clone(), generic_access_req_stream };
fasync::Task::spawn(service.run()).detach();
(service_client, dispatcher)
}
#[fuchsia::test]
async fn test_change_name() {
let (delegate_client, host_dispatcher) = setup_generic_access_service();
let expected_device_name = delegate_client
.read_value(
&PeerId { value: 1 },
&gatt::Handle { value: GENERIC_ACCESS_DEVICE_NAME_ID },
0,
)
.await
.unwrap()
.unwrap();
assert_eq!(expected_device_name, DEFAULT_DEVICE_NAME.as_bytes());
// This is expected to error since there is no host.
let _ = host_dispatcher
.set_name("test-generic-access-service-1".to_string(), NameReplace::Replace)
.await
.unwrap_err();
let expected_device_name = delegate_client
.read_value(
&PeerId { value: 1 },
&gatt::Handle { value: GENERIC_ACCESS_DEVICE_NAME_ID },
0,
)
.await
.unwrap()
.unwrap();
assert_eq!(expected_device_name, "test-generic-access-service-1".to_string().as_bytes());
}
#[fuchsia::test]
async fn test_get_appearance() {
let (service_client, _host_dispatcher) = setup_generic_access_service();
let read_device_appearance = service_client
.read_value(
&PeerId { value: 1 },
&gatt::Handle { value: GENERIC_ACCESS_APPEARANCE_ID },
0,
)
.await
.unwrap()
.unwrap();
assert_eq!(
read_device_appearance,
TEST_DEVICE_APPEARANCE.into_primitive().to_le_bytes().to_vec()
);
}
#[fuchsia::test]
async fn test_invalid_request() {
let (service_client, _host_dispatcher) = setup_generic_access_service();
let result = service_client
.write_value(&gatt::LocalServiceWriteValueRequest {
peer_id: Some(PeerId { value: 1 }),
handle: Some(gatt::Handle { value: GENERIC_ACCESS_DEVICE_NAME_ID }),
offset: Some(0),
value: Some(b"new-name".to_vec()),
..Default::default()
})
.await
.unwrap();
assert!(result.is_err());
assert_eq!(result.unwrap_err(), gatt::Error::WriteNotPermitted);
}
#[fuchsia::test]
async fn test_gas_proxy() {
let (service_client, service_request_stream) =
create_proxy_and_stream::<gatt::LocalServiceMarker>().unwrap();
let (gas_task_channel, mut generic_access_req_stream) =
mpsc::channel::<gatt::LocalServiceRequest>(0);
let (gatt_server, _gatt_server_remote) =
create_proxy_and_stream::<gatt::Server_Marker>().unwrap();
let gas_proxy =
GasProxy { service_request_stream, gas_task_channel, _gatt_server: gatt_server };
fasync::Task::spawn(gas_proxy.run().map(|r| {
r.unwrap_or_else(|err| {
warn!("Error running Generic Access proxy in task: {:?}", err);
})
}))
.detach();
let _ignored_fut = service_client.read_value(
&PeerId { value: 1 },
&gatt::Handle { value: GENERIC_ACCESS_APPEARANCE_ID },
0,
);
let proxied_request = generic_access_req_stream.next().await.unwrap();
let (_, handle, ..) = proxied_request.into_read_value().expect("ReadValue request");
assert_eq!(handle.value, GENERIC_ACCESS_APPEARANCE_ID);
}
}