blob: 8b552c16d99a8d4dc2cdc2db1063627dd13dc625 [file] [log] [blame]
// Copyright 2024 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::Error;
use bt_map::{Error as MapError, *};
use bt_obex::profile::{
goep_l2cap_psm_attribute, parse_obex_search_result, GOEP_L2CAP_PSM_ATTRIBUTE,
};
use fidl_fuchsia_bluetooth_bredr as bredr;
use fuchsia_bluetooth::profile::*;
use fuchsia_bluetooth::types::Uuid;
use profile_client::ProfileClient;
use std::collections::HashSet;
use tracing::info;
const PROFILE_MAJOR_VERSION: u8 = 1;
const PROFILE_MINOR_VERSION: u8 = 4;
const MNS_SERVICE_NAME: &str = "MAP MNS-Fuchsia";
/// SDP Attribute ID for MapSupportedFeatures.
/// https://www.bluetooth.com/specifications/assigned-numbers/service-discovery
const ATTR_MAS_INSTANCE_ID: u16 = 0x0315;
const ATTR_SUPPORTED_MESSAGE_TYPES: u16 = 0x0316;
const ATTR_MAP_SUPPORTED_FEATURES: u16 = 0x0317;
/// Human readable attribute for the service name.
/// According to Core v5.3 vol. 3 part B section 5.1.8,
/// human readable attributes IDs are in the range 0x0100 to 0x01FF
/// and from Assigned Numbers section 5.2, the ID offset for ServiceName is 0x0000.
pub const ATTR_SERVICE_NAME: u16 = 0x100;
const FALLBACK_MAS_SERVICE_NAME: &str = "MAP MAS instance";
/// Represents a single Message Access Server (MAS) instance at a MSE peer.
/// A MSE peer may have one or more MAS instances.
/// MCE can access each MAS Instance by a dedicated OBEX connection.
/// See MAP v1.4.2 section 7.1.1 for deetails.
#[derive(Debug, PartialEq)]
pub struct MasConfig {
instance_id: u8, // ID that identifies this MAS instance.
name: String,
supported_message_types: HashSet<MessageType>,
connection: bredr::ConnectParameters,
features: MapSupportedFeatures,
}
impl MasConfig {
/// Cross checks the profile search result with the service definition for
/// Message Access Service as listed in MAP v1.4.2 section 7.1.1.
pub fn from_search_result(
protocol: Vec<bredr::ProtocolDescriptor>,
attributes: Vec<bredr::Attribute>,
) -> Result<Self, MapError> {
// Ensure MAS service is advertised.
let service_ids = find_service_classes(&attributes);
if service_ids
.iter()
.find(|id| {
id.number
== bredr::ServiceClassProfileIdentifier::MessageAccessServer.into_primitive()
})
.is_none()
{
return Err(MapError::DoesNotExist(ServiceRecordItem::MasServiceClassId));
}
// Ensure MAP profile is advertised.
let profile_desc = find_profile_descriptors(&attributes)
.map_err(|_| MapError::DoesNotExist(ServiceRecordItem::MapProfileDescriptor))?;
if profile_desc
.iter()
.find(|desc| {
desc.profile_id == Some(bredr::ServiceClassProfileIdentifier::MessageAccessProfile)
})
.is_none()
{
return Err(MapError::DoesNotExist(ServiceRecordItem::MapProfileDescriptor));
}
let protocol = protocol
.iter()
.map(|p| ProtocolDescriptor::try_from(p))
.collect::<Result<Vec<_>, _>>()
.map_err(|_| MapError::InvalidParameters)?;
let attributes = attributes
.iter()
.map(|a| Attribute::try_from(a))
.collect::<Result<Vec<_>, _>>()
.map_err(|_| MapError::InvalidParameters)?;
// Ensure either L2CAP or RFCOMM connection is supported.
let connection = parse_obex_search_result(&protocol, &attributes)
.ok_or(MapError::NotGoepInteroperable)?;
// Get information about this MAS instance.
let id = attributes
.iter()
.find_map(|a| {
if a.id != ATTR_MAS_INSTANCE_ID {
return None;
}
let DataElement::Uint8(raw_val) = a.element else {
return None;
};
Some(raw_val)
})
.ok_or(MapError::DoesNotExist(ServiceRecordItem::MasInstanceId))?;
let name = attributes
.iter()
.find_map(|a| {
// TODO(b/328074442): once getting languaged-based
// attributes is supported, get the attributes through
// that instead.
if a.id != ATTR_SERVICE_NAME {
return None;
}
match &a.element {
DataElement::Str(bytes) => String::from_utf8(bytes.to_vec())
.ok()
.or(Some(FALLBACK_MAS_SERVICE_NAME.to_string())),
_ => Some(FALLBACK_MAS_SERVICE_NAME.to_string()),
}
})
.ok_or(MapError::DoesNotExist(ServiceRecordItem::ServiceName))?;
// Get supported times
let supported_message_types = attributes
.iter()
.find_map(|a| {
if a.id != ATTR_SUPPORTED_MESSAGE_TYPES {
return None;
}
let DataElement::Uint8(raw_val) = a.element else {
return None;
};
let supported: Vec<Result<MessageType, MapError>> =
MessageType::from_bits(raw_val).collect();
let supported: HashSet<MessageType> =
supported.into_iter().filter_map(|r| r.ok()).collect();
Some(supported)
})
.ok_or(MapError::DoesNotExist(ServiceRecordItem::SupportedMessageTypes))?;
// We intersect the features supported by Sapphire and the features supported by
// the peer device.
let features = attributes
.iter()
.find_map(|a| {
if a.id != ATTR_MAP_SUPPORTED_FEATURES {
return None;
}
let DataElement::Uint32(raw_val) = a.element else {
return None;
};
Some(MapSupportedFeatures::from_bits_truncate(raw_val))
})
.ok_or(MapError::DoesNotExist(ServiceRecordItem::MapSupportedFeatures))?;
let config =
MasConfig { instance_id: id, name, supported_message_types, connection, features };
Ok(config)
}
}
fn default_map_supported_features() -> MapSupportedFeatures {
MapSupportedFeatures::NOTIFICATION_REGISTRATION
| MapSupportedFeatures::NOTIFICATION
| MapSupportedFeatures::BROWSING
| MapSupportedFeatures::EXTENDED_EVENT_REPORT_1_1
| MapSupportedFeatures::MESSAGES_LISTING_FORMAT_VERSION_1_1
| MapSupportedFeatures::MAPSUPPORTEDFEATURES_IN_CONNECT_REQUEST
}
/// Service definition for Message Notification Service on the MCE device.
/// See MAP v.1.4.2, Section 7.1.2 for details.
fn build_mns_service_definition() -> ServiceDefinition {
ServiceDefinition {
service_class_uuids: vec![Uuid::new16(
bredr::ServiceClassProfileIdentifier::MessageNotificationServer.into_primitive(),
)
.into()],
protocol_descriptor_list: vec![
ProtocolDescriptor { protocol: bredr::ProtocolIdentifier::L2Cap, params: vec![] },
ProtocolDescriptor {
protocol: bredr::ProtocolIdentifier::Rfcomm,
// Note: RFCOMM channel number is assigned by bt-rfcomm so we don't include it in
// the parameter.
params: vec![],
},
ProtocolDescriptor { protocol: bredr::ProtocolIdentifier::Obex, params: vec![] },
],
information: vec![Information {
language: "en".to_string(),
// TODO(b/328115144): consider making this configurable
// through structured configs.
name: Some(MNS_SERVICE_NAME.to_string()),
description: None,
provider: None,
}],
profile_descriptors: vec![bredr::ProfileDescriptor {
profile_id: Some(bredr::ServiceClassProfileIdentifier::MessageAccessProfile),
major_version: Some(PROFILE_MAJOR_VERSION),
minor_version: Some(PROFILE_MINOR_VERSION),
..Default::default()
}],
additional_attributes: vec![
// Request a dynamic PSM to be assigned by the profile service.
goep_l2cap_psm_attribute(Psm::new(bredr::PSM_DYNAMIC)),
Attribute {
id: ATTR_MAP_SUPPORTED_FEATURES,
element: DataElement::Uint32(default_map_supported_features().bits()),
},
],
..Default::default()
}
}
pub fn connect_and_advertise(profile_svc: bredr::ProfileProxy) -> Result<ProfileClient, Error> {
// Attributes to search for in SDP record for the Message Access Service on a MSE device.
let search_attributes = vec![
bredr::ATTR_SERVICE_CLASS_ID_LIST,
bredr::ATTR_PROTOCOL_DESCRIPTOR_LIST,
ATTR_SERVICE_NAME,
bredr::ATTR_BLUETOOTH_PROFILE_DESCRIPTOR_LIST,
GOEP_L2CAP_PSM_ATTRIBUTE,
ATTR_MAS_INSTANCE_ID,
ATTR_SUPPORTED_MESSAGE_TYPES,
ATTR_MAP_SUPPORTED_FEATURES,
];
let service_defs = vec![(&build_mns_service_definition()).try_into()?];
let channel_parameters = bredr::ChannelParameters {
channel_mode: Some(bredr::ChannelMode::EnhancedRetransmission),
..Default::default()
};
// MCE device advertises the MNS on it and and searches for MAS on remote peers.
let mut profile_client =
ProfileClient::advertise(profile_svc.clone(), service_defs, channel_parameters)?;
profile_client.add_search(
bredr::ServiceClassProfileIdentifier::MessageAccessServer,
Some(search_attributes),
)?;
info!("Registered service search & advertisement");
Ok(profile_client)
}
#[cfg(test)]
mod tests {
use super::*;
/// Returns protocols with RFCOMM and OBEX protocols for testing purposes.
fn test_protocols() -> Vec<bredr::ProtocolDescriptor> {
vec![
bredr::ProtocolDescriptor {
protocol: Some(bredr::ProtocolIdentifier::Rfcomm),
params: Some(vec![bredr::DataElement::Uint8(8)]),
..Default::default()
},
bredr::ProtocolDescriptor {
protocol: Some(bredr::ProtocolIdentifier::Obex),
params: Some(vec![]),
..Default::default()
},
]
}
/// Returns attributes for testing purposes. Contains all the attributes for a MAS service record
/// except for the GoepL2CapPsm attribute.
fn test_attributes() -> Vec<bredr::Attribute> {
vec![
bredr::Attribute {
id: Some(bredr::ATTR_SERVICE_CLASS_ID_LIST),
element: Some(bredr::DataElement::Sequence(vec![Some(Box::new(
bredr::DataElement::Uuid(
Uuid::new16(
bredr::ServiceClassProfileIdentifier::MessageAccessServer
.into_primitive(),
)
.into(),
),
))])),
..Default::default()
},
bredr::Attribute {
id: Some(bredr::ATTR_BLUETOOTH_PROFILE_DESCRIPTOR_LIST),
element: Some(bredr::DataElement::Sequence(vec![Some(Box::new(
bredr::DataElement::Sequence(vec![
Some(Box::new(bredr::DataElement::Uuid(
Uuid::new16(
bredr::ServiceClassProfileIdentifier::MessageAccessProfile
.into_primitive(),
)
.into(),
))),
Some(Box::new(bredr::DataElement::Uint16(0x0104))),
]),
))])),
..Default::default()
},
bredr::Attribute {
id: Some(ATTR_SERVICE_NAME),
element: Some(bredr::DataElement::Str(vec![0x68, 0x69])),
..Default::default()
},
bredr::Attribute {
id: Some(ATTR_MAS_INSTANCE_ID),
element: Some(bredr::DataElement::Uint8(1)),
..Default::default()
},
bredr::Attribute {
id: Some(ATTR_SUPPORTED_MESSAGE_TYPES),
element: Some(bredr::DataElement::Uint8(0x05)), // email and sms cdma.
..Default::default()
},
bredr::Attribute {
id: Some(ATTR_MAP_SUPPORTED_FEATURES),
element: Some(bredr::DataElement::Uint32(0x00080007)),
..Default::default()
},
]
}
#[test]
fn mas_config_from_search_result_rfcomm() {
let config = MasConfig::from_search_result(test_protocols(), test_attributes())
.expect("should succeed");
match config.connection {
bredr::ConnectParameters::L2cap(_) => panic!("should not be L2cap"),
bredr::ConnectParameters::Rfcomm(chan) => assert_eq!(chan.channel, Some(8)),
};
assert!(config.features.contains(MapSupportedFeatures::NOTIFICATION_REGISTRATION));
assert!(config.features.contains(MapSupportedFeatures::NOTIFICATION));
assert!(config.features.contains(MapSupportedFeatures::BROWSING));
assert!(config
.features
.contains(MapSupportedFeatures::MAPSUPPORTEDFEATURES_IN_CONNECT_REQUEST));
assert_eq!(config.instance_id, 1);
assert_eq!(config.name, "hi".to_string());
assert_eq!(
config.supported_message_types,
HashSet::from([MessageType::Email, MessageType::SmsCdma])
)
}
#[test]
fn mas_config_from_search_result_l2cap() {
let mut protocols = test_protocols();
// Add L2CAP protocol.
protocols.push(bredr::ProtocolDescriptor {
protocol: Some(bredr::ProtocolIdentifier::L2Cap),
params: Some(vec![]),
..Default::default()
});
let mut attributes = test_attributes();
// Set service name attribute to an invalid value to test fall back name.
for a in attributes.iter_mut() {
if a.id == Some(ATTR_SERVICE_NAME) {
a.element = Some(bredr::DataElement::Uint8(0x00));
}
}
// Add GOEP L2CAP Psm attribute to test GOEP interoperability.
attributes.push(bredr::Attribute {
id: Some(GOEP_L2CAP_PSM_ATTRIBUTE),
element: Some(bredr::DataElement::Uint16(0x1007)),
..Default::default()
});
let config = MasConfig::from_search_result(protocols, attributes).expect("should succeed");
match config.connection {
bredr::ConnectParameters::L2cap(chan) => assert_eq!(chan.psm, Some(0x1007)),
bredr::ConnectParameters::Rfcomm(_) => panic!("should not be Rfcomm"),
};
assert_eq!(config.name, FALLBACK_MAS_SERVICE_NAME.to_string());
}
#[test]
fn mas_config_from_search_result_rfu_message_types() {
let protocols = test_protocols();
let mut attributes = test_attributes();
// Set RFU bits in supported message types attribute.
for a in attributes.iter_mut() {
if a.id == Some(ATTR_SUPPORTED_MESSAGE_TYPES) {
a.element = Some(bredr::DataElement::Uint8(0b10000101)); // email and sms cdma and bit 7.
}
}
let config = MasConfig::from_search_result(protocols, attributes).expect("should succeed");
assert_eq!(
config.supported_message_types,
HashSet::from([MessageType::Email, MessageType::SmsCdma])
);
}
#[test]
fn mas_config_from_search_result_fail() {
// Missing obex.
let _ = MasConfig::from_search_result(
vec![bredr::ProtocolDescriptor {
protocol: Some(bredr::ProtocolIdentifier::Rfcomm),
params: Some(vec![bredr::DataElement::Uint8(8)]),
..Default::default()
}],
vec![
bredr::Attribute {
id: Some(bredr::ATTR_SERVICE_CLASS_ID_LIST),
element: Some(bredr::DataElement::Sequence(vec![Some(Box::new(
bredr::DataElement::Uuid(
Uuid::new16(
bredr::ServiceClassProfileIdentifier::MessageAccessServer
.into_primitive(),
)
.into(),
),
))])),
..Default::default()
},
bredr::Attribute {
id: Some(bredr::ATTR_BLUETOOTH_PROFILE_DESCRIPTOR_LIST),
element: Some(bredr::DataElement::Sequence(vec![Some(Box::new(
bredr::DataElement::Sequence(vec![
Some(Box::new(bredr::DataElement::Uuid(
Uuid::new16(
bredr::ServiceClassProfileIdentifier::MessageAccessProfile
.into_primitive(),
)
.into(),
))),
Some(Box::new(bredr::DataElement::Uint16(0x0104))),
]),
))])),
..Default::default()
},
bredr::Attribute {
id: Some(ATTR_SERVICE_NAME),
element: Some(bredr::DataElement::Str(vec![0x68, 0x69])),
..Default::default()
},
bredr::Attribute {
id: Some(ATTR_MAS_INSTANCE_ID),
element: Some(bredr::DataElement::Uint8(1)),
..Default::default()
},
bredr::Attribute {
id: Some(ATTR_SUPPORTED_MESSAGE_TYPES),
element: Some(bredr::DataElement::Uint8(0x05)), // email and sms cdma.
..Default::default()
},
bredr::Attribute {
id: Some(ATTR_MAP_SUPPORTED_FEATURES),
element: Some(bredr::DataElement::Uint32(0x00080007)),
..Default::default()
},
],
)
.expect_err("should fail");
// Missing MAS instance ID.
let _ = MasConfig::from_search_result(
vec![
bredr::ProtocolDescriptor {
protocol: Some(bredr::ProtocolIdentifier::L2Cap),
params: Some(vec![]),
..Default::default()
},
bredr::ProtocolDescriptor {
protocol: Some(bredr::ProtocolIdentifier::Rfcomm),
params: Some(vec![bredr::DataElement::Uint8(8)]),
..Default::default()
},
bredr::ProtocolDescriptor {
protocol: Some(bredr::ProtocolIdentifier::Obex),
params: Some(vec![]),
..Default::default()
},
],
vec![
bredr::Attribute {
id: Some(bredr::ATTR_SERVICE_CLASS_ID_LIST),
element: Some(bredr::DataElement::Sequence(vec![Some(Box::new(
bredr::DataElement::Uuid(
Uuid::new16(
bredr::ServiceClassProfileIdentifier::MessageAccessServer
.into_primitive(),
)
.into(),
),
))])),
..Default::default()
},
bredr::Attribute {
id: Some(bredr::ATTR_BLUETOOTH_PROFILE_DESCRIPTOR_LIST),
element: Some(bredr::DataElement::Sequence(vec![Some(Box::new(
bredr::DataElement::Sequence(vec![
Some(Box::new(bredr::DataElement::Uuid(
Uuid::new16(
bredr::ServiceClassProfileIdentifier::MessageAccessProfile
.into_primitive(),
)
.into(),
))),
Some(Box::new(bredr::DataElement::Uint16(0x0104))),
]),
))])),
..Default::default()
},
bredr::Attribute {
id: Some(GOEP_L2CAP_PSM_ATTRIBUTE),
element: Some(bredr::DataElement::Uint16(0x1007)),
..Default::default()
},
bredr::Attribute {
id: Some(ATTR_SERVICE_NAME),
element: Some(bredr::DataElement::Uint8(0x00)),
..Default::default()
},
bredr::Attribute {
id: Some(ATTR_SUPPORTED_MESSAGE_TYPES),
element: Some(bredr::DataElement::Uint8(0x05)), // email and sms cdma.
..Default::default()
},
bredr::Attribute {
id: Some(ATTR_MAP_SUPPORTED_FEATURES),
element: Some(bredr::DataElement::Uint32(0x00080007)),
..Default::default()
},
],
)
.expect_err("should fail");
}
}