| /* Copyright (c) 2013 The Chromium OS Authors. All rights reserved. |
| * Use of this source code is governed by a BSD-style license that can be |
| * found in the LICENSE file. |
| */ |
| |
| #include <stdint.h> |
| #include <syslog.h> |
| #include <sys/socket.h> |
| #include <unistd.h> |
| |
| #include "cras_a2dp_endpoint.h" |
| #include "cras_bt_adapter.h" |
| #include "cras_bt_constants.h" |
| #include "cras_bt_log.h" |
| #include "cras_bt_profile.h" |
| #include "cras_hfp_ag_profile.h" |
| #include "cras_hfp_info.h" |
| #include "cras_hfp_iodev.h" |
| #include "cras_hfp_alsa_iodev.h" |
| #include "cras_server_metrics.h" |
| #include "cras_system_state.h" |
| #include "cras_iodev_list.h" |
| #include "utlist.h" |
| #include "packet_status_logger.h" |
| |
| #define HFP_AG_PROFILE_NAME "Hands-Free Voice gateway" |
| #define HFP_AG_PROFILE_PATH "/org/chromium/Cras/Bluetooth/HFPAG" |
| #define HFP_VERSION 0x0107 |
| #define HSP_AG_PROFILE_NAME "Headset Voice gateway" |
| #define HSP_AG_PROFILE_PATH "/org/chromium/Cras/Bluetooth/HSPAG" |
| #define HSP_VERSION_1_2 0x0102 |
| #define HSP_VERSION_1_2_STR "0x0102" |
| |
| #define HSP_AG_RECORD \ |
| "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>" \ |
| "<record>" \ |
| " <attribute id=\"0x0001\">" \ |
| " <sequence>" \ |
| " <uuid value=\"" HSP_AG_UUID "\" />" \ |
| " <uuid value=\"" GENERIC_AUDIO_UUID "\" />" \ |
| " </sequence>" \ |
| " </attribute>" \ |
| " <attribute id=\"0x0004\">" \ |
| " <sequence>" \ |
| " <sequence>" \ |
| " <uuid value=\"0x0100\" />" \ |
| " </sequence>" \ |
| " <sequence>" \ |
| " <uuid value=\"0x0003\" />" \ |
| " <uint8 value=\"0x0c\" />" \ |
| " </sequence>" \ |
| " </sequence>" \ |
| " </attribute>" \ |
| " <attribute id=\"0x0005\">" \ |
| " <sequence>" \ |
| " <uuid value=\"0x1002\" />" \ |
| " </sequence>" \ |
| " </attribute>" \ |
| " <attribute id=\"0x0009\">" \ |
| " <sequence>" \ |
| " <sequence>" \ |
| " <uuid value=\"" HSP_HS_UUID "\" />" \ |
| " <uint16 value=\"" HSP_VERSION_1_2_STR "\" />" \ |
| " </sequence>" \ |
| " </sequence>" \ |
| " </attribute>" \ |
| " <attribute id=\"0x0100\">" \ |
| " <text value=\"" HSP_AG_PROFILE_NAME "\" />" \ |
| " </attribute>" \ |
| " <attribute id=\"0x0301\" >" \ |
| " <uint8 value=\"0x01\" />" \ |
| " </attribute>" \ |
| "</record>" |
| |
| /* The supported features value in +BSRF command response of HFP AG in CRAS */ |
| #define BSRF_SUPPORTED_FEATURES (AG_ENHANCED_CALL_STATUS | AG_HF_INDICATORS) |
| |
| /* The "SupportedFeatures" attribute value of HFP AG service record in CRAS. */ |
| #define SDP_SUPPORTED_FEATURES FEATURES_AG_WIDE_BAND_SPEECH |
| |
| /* Object representing the audio gateway role for HFP/HSP. |
| * Members: |
| * idev - The input iodev for HFP/HSP. |
| * odev - The output iodev for HFP/HSP. |
| * info - The hfp_info object for SCO audio. |
| * slc_handle - The service level connection. |
| * device - The bt device associated with this audio gateway. |
| * a2dp_delay_retries - The number of retries left to delay starting |
| * the hfp/hsp audio gateway to wait for a2dp connection. |
| * conn - The dbus connection used to send message to bluetoothd. |
| * profile - The profile enum of this audio gateway. |
| */ |
| struct audio_gateway { |
| struct cras_iodev *idev; |
| struct cras_iodev *odev; |
| struct hfp_info *info; |
| struct hfp_slc_handle *slc_handle; |
| struct cras_bt_device *device; |
| int a2dp_delay_retries; |
| DBusConnection *conn; |
| enum cras_bt_device_profile profile; |
| struct audio_gateway *prev, *next; |
| }; |
| |
| static struct audio_gateway *connected_ags; |
| static struct packet_status_logger wbs_logger; |
| |
| static int need_go_sco_pcm(struct cras_bt_device *device) |
| { |
| return cras_iodev_list_get_sco_pcm_iodev(CRAS_STREAM_INPUT) || |
| cras_iodev_list_get_sco_pcm_iodev(CRAS_STREAM_OUTPUT); |
| } |
| |
| static void destroy_audio_gateway(struct audio_gateway *ag) |
| { |
| DL_DELETE(connected_ags, ag); |
| |
| cras_server_metrics_hfp_battery_indicator( |
| hfp_slc_get_hf_supports_battery_indicator(ag->slc_handle)); |
| |
| if (need_go_sco_pcm(ag->device)) { |
| if (ag->idev) |
| hfp_alsa_iodev_destroy(ag->idev); |
| if (ag->odev) |
| hfp_alsa_iodev_destroy(ag->odev); |
| } else { |
| if (ag->idev) |
| hfp_iodev_destroy(ag->idev); |
| if (ag->odev) |
| hfp_iodev_destroy(ag->odev); |
| } |
| |
| if (ag->info) { |
| if (hfp_info_running(ag->info)) |
| hfp_info_stop(ag->info); |
| hfp_info_destroy(ag->info); |
| } |
| if (ag->slc_handle) |
| hfp_slc_destroy(ag->slc_handle); |
| |
| free(ag); |
| } |
| |
| /* Checks if there already a audio gateway connected for device. */ |
| static int has_audio_gateway(struct cras_bt_device *device) |
| { |
| struct audio_gateway *ag; |
| DL_FOREACH (connected_ags, ag) { |
| if (ag->device == device) |
| return 1; |
| } |
| return 0; |
| } |
| |
| static void cras_hfp_ag_release(struct cras_bt_profile *profile) |
| { |
| struct audio_gateway *ag; |
| |
| DL_FOREACH (connected_ags, ag) |
| destroy_audio_gateway(ag); |
| } |
| |
| /* Callback triggered when SLC is initialized. */ |
| static int cras_hfp_ag_slc_initialized(struct hfp_slc_handle *handle) |
| { |
| struct audio_gateway *ag; |
| |
| DL_SEARCH_SCALAR(connected_ags, ag, slc_handle, handle); |
| if (!ag) |
| return -EINVAL; |
| |
| /* Log if the hands-free device supports WBS or not. Assuming the |
| * codec negotiation feature means the WBS capability on headset. |
| */ |
| cras_server_metrics_hfp_wideband_support( |
| hfp_slc_get_hf_codec_negotiation_supported(handle)); |
| |
| /* Log the final selected codec given that codec negotiation is |
| * supported. |
| */ |
| if (hfp_slc_get_hf_codec_negotiation_supported(handle) && |
| hfp_slc_get_ag_codec_negotiation_supported(handle)) |
| cras_server_metrics_hfp_wideband_selected_codec( |
| hfp_slc_get_selected_codec(handle)); |
| |
| /* Defer the starting of audio gateway to bt_device. */ |
| return cras_bt_device_audio_gateway_initialized(ag->device); |
| } |
| |
| static int cras_hfp_ag_slc_disconnected(struct hfp_slc_handle *handle) |
| { |
| struct audio_gateway *ag; |
| |
| DL_SEARCH_SCALAR(connected_ags, ag, slc_handle, handle); |
| if (!ag) |
| return -EINVAL; |
| |
| destroy_audio_gateway(ag); |
| cras_bt_device_notify_profile_dropped( |
| ag->device, CRAS_BT_DEVICE_PROFILE_HFP_HANDSFREE); |
| return 0; |
| } |
| |
| static int check_for_conflict_ag(struct cras_bt_device *new_connected) |
| { |
| struct audio_gateway *ag; |
| |
| /* Check if there's already an A2DP/HFP device. */ |
| DL_FOREACH (connected_ags, ag) { |
| if (cras_bt_device_has_a2dp(ag->device)) |
| return -1; |
| } |
| |
| /* Check if there's already an A2DP-only device. */ |
| if (cras_a2dp_connected_device() && |
| cras_bt_device_supports_profile(new_connected, |
| CRAS_BT_DEVICE_PROFILE_A2DP_SINK)) |
| return -1; |
| |
| return 0; |
| } |
| |
| int cras_hfp_ag_remove_conflict(struct cras_bt_device *device) |
| { |
| struct audio_gateway *ag; |
| |
| DL_FOREACH (connected_ags, ag) { |
| if (ag->device == device) |
| continue; |
| cras_bt_device_notify_profile_dropped( |
| ag->device, CRAS_BT_DEVICE_PROFILE_HFP_HANDSFREE); |
| destroy_audio_gateway(ag); |
| } |
| return 0; |
| } |
| |
| static int cras_hfp_ag_new_connection(DBusConnection *conn, |
| struct cras_bt_profile *profile, |
| struct cras_bt_device *device, |
| int rfcomm_fd) |
| { |
| struct cras_bt_adapter *adapter; |
| struct audio_gateway *ag; |
| int ag_features; |
| |
| BTLOG(btlog, BT_HFP_NEW_CONNECTION, 0, 0); |
| |
| if (has_audio_gateway(device)) { |
| syslog(LOG_ERR, |
| "Audio gateway exists when %s connects for profile %s", |
| cras_bt_device_name(device), profile->name); |
| close(rfcomm_fd); |
| return 0; |
| } |
| |
| if (check_for_conflict_ag(device)) |
| return -1; |
| |
| ag = (struct audio_gateway *)calloc(1, sizeof(*ag)); |
| ag->device = device; |
| ag->conn = conn; |
| ag->profile = cras_bt_device_profile_from_uuid(profile->uuid); |
| |
| adapter = cras_bt_device_adapter(device); |
| /* |
| * If the WBS enabled flag is set and adapter reports wbs capability |
| * then add codec negotiation feature. |
| * TODO(hychao): AND the two conditions to let bluetooth daemon |
| * control whether to turn on WBS feature. |
| */ |
| ag_features = BSRF_SUPPORTED_FEATURES; |
| if (cras_system_get_bt_wbs_enabled() && adapter && |
| cras_bt_adapter_wbs_supported(adapter)) |
| ag_features |= AG_CODEC_NEGOTIATION; |
| |
| ag->slc_handle = hfp_slc_create(rfcomm_fd, 0, ag_features, device, |
| cras_hfp_ag_slc_initialized, |
| cras_hfp_ag_slc_disconnected); |
| DL_APPEND(connected_ags, ag); |
| return 0; |
| } |
| |
| static void cras_hfp_ag_request_disconnection(struct cras_bt_profile *profile, |
| struct cras_bt_device *device) |
| { |
| struct audio_gateway *ag; |
| |
| BTLOG(btlog, BT_HFP_REQUEST_DISCONNECT, 0, 0); |
| |
| DL_FOREACH (connected_ags, ag) { |
| if (ag->slc_handle && ag->device == device) { |
| cras_bt_device_notify_profile_dropped( |
| ag->device, |
| CRAS_BT_DEVICE_PROFILE_HFP_HANDSFREE); |
| destroy_audio_gateway(ag); |
| } |
| } |
| } |
| |
| static void cras_hfp_ag_cancel(struct cras_bt_profile *profile) |
| { |
| } |
| |
| static struct cras_bt_profile cras_hfp_ag_profile = { |
| .name = HFP_AG_PROFILE_NAME, |
| .object_path = HFP_AG_PROFILE_PATH, |
| .uuid = HFP_AG_UUID, |
| .version = HFP_VERSION, |
| .role = NULL, |
| .features = SDP_SUPPORTED_FEATURES, |
| .record = NULL, |
| .release = cras_hfp_ag_release, |
| .new_connection = cras_hfp_ag_new_connection, |
| .request_disconnection = cras_hfp_ag_request_disconnection, |
| .cancel = cras_hfp_ag_cancel |
| }; |
| |
| int cras_hfp_ag_profile_create(DBusConnection *conn) |
| { |
| return cras_bt_add_profile(conn, &cras_hfp_ag_profile); |
| } |
| |
| static int cras_hsp_ag_new_connection(DBusConnection *conn, |
| struct cras_bt_profile *profile, |
| struct cras_bt_device *device, |
| int rfcomm_fd) |
| { |
| struct audio_gateway *ag; |
| |
| BTLOG(btlog, BT_HSP_NEW_CONNECTION, 0, 0); |
| |
| if (has_audio_gateway(device)) { |
| syslog(LOG_ERR, |
| "Audio gateway exists when %s connects for profile %s", |
| cras_bt_device_name(device), profile->name); |
| close(rfcomm_fd); |
| return 0; |
| } |
| |
| if (check_for_conflict_ag(device)) |
| return -1; |
| |
| ag = (struct audio_gateway *)calloc(1, sizeof(*ag)); |
| ag->device = device; |
| ag->conn = conn; |
| ag->profile = cras_bt_device_profile_from_uuid(profile->uuid); |
| ag->slc_handle = |
| hfp_slc_create(rfcomm_fd, 1, BSRF_SUPPORTED_FEATURES, device, |
| NULL, cras_hfp_ag_slc_disconnected); |
| DL_APPEND(connected_ags, ag); |
| cras_hfp_ag_slc_initialized(ag->slc_handle); |
| return 0; |
| } |
| |
| static void cras_hsp_ag_request_disconnection(struct cras_bt_profile *profile, |
| struct cras_bt_device *device) |
| { |
| struct audio_gateway *ag; |
| |
| BTLOG(btlog, BT_HSP_REQUEST_DISCONNECT, 0, 0); |
| |
| DL_FOREACH (connected_ags, ag) { |
| if (ag->slc_handle && ag->device == device) { |
| cras_bt_device_notify_profile_dropped( |
| ag->device, CRAS_BT_DEVICE_PROFILE_HSP_HEADSET); |
| destroy_audio_gateway(ag); |
| } |
| } |
| } |
| |
| static struct cras_bt_profile cras_hsp_ag_profile = { |
| .name = HSP_AG_PROFILE_NAME, |
| .object_path = HSP_AG_PROFILE_PATH, |
| .uuid = HSP_AG_UUID, |
| .version = HSP_VERSION_1_2, |
| .role = NULL, |
| .record = HSP_AG_RECORD, |
| .release = cras_hfp_ag_release, |
| .new_connection = cras_hsp_ag_new_connection, |
| .request_disconnection = cras_hsp_ag_request_disconnection, |
| .cancel = cras_hfp_ag_cancel |
| }; |
| |
| int cras_hfp_ag_start(struct cras_bt_device *device) |
| { |
| struct audio_gateway *ag; |
| |
| BTLOG(btlog, BT_AUDIO_GATEWAY_START, 0, 0); |
| |
| DL_SEARCH_SCALAR(connected_ags, ag, device, device); |
| if (ag == NULL) |
| return -EEXIST; |
| |
| /* |
| * There is chance that bluetooth stack notifies us about remote |
| * device's capability incrementally in multiple events. That could |
| * cause hfp_ag_start be called more than once. Check if the input |
| * HFP iodev is already created so we don't re-create HFP resources. |
| */ |
| if (ag->idev) |
| return 0; |
| |
| if (need_go_sco_pcm(device)) { |
| struct cras_iodev *in_aio, *out_aio; |
| |
| in_aio = cras_iodev_list_get_sco_pcm_iodev(CRAS_STREAM_INPUT); |
| out_aio = cras_iodev_list_get_sco_pcm_iodev(CRAS_STREAM_OUTPUT); |
| |
| ag->idev = hfp_alsa_iodev_create(in_aio, ag->device, |
| ag->slc_handle, ag->profile); |
| ag->odev = hfp_alsa_iodev_create(out_aio, ag->device, |
| ag->slc_handle, ag->profile); |
| } else { |
| ag->info = hfp_info_create(); |
| hfp_info_set_wbs_logger(ag->info, &wbs_logger); |
| ag->idev = |
| hfp_iodev_create(CRAS_STREAM_INPUT, ag->device, |
| ag->slc_handle, ag->profile, ag->info); |
| ag->odev = |
| hfp_iodev_create(CRAS_STREAM_OUTPUT, ag->device, |
| ag->slc_handle, ag->profile, ag->info); |
| } |
| |
| if (!ag->idev && !ag->odev) { |
| destroy_audio_gateway(ag); |
| return -ENOMEM; |
| } |
| |
| return 0; |
| } |
| |
| void cras_hfp_ag_suspend_connected_device(struct cras_bt_device *device) |
| { |
| struct audio_gateway *ag; |
| |
| DL_SEARCH_SCALAR(connected_ags, ag, device, device); |
| if (ag) |
| destroy_audio_gateway(ag); |
| } |
| |
| struct hfp_slc_handle *cras_hfp_ag_get_active_handle() |
| { |
| /* Returns the first handle for HFP qualification. In future we |
| * might want this to return the HFP device user is selected. */ |
| return connected_ags ? connected_ags->slc_handle : NULL; |
| } |
| |
| struct hfp_slc_handle *cras_hfp_ag_get_slc(struct cras_bt_device *device) |
| { |
| struct audio_gateway *ag; |
| DL_FOREACH (connected_ags, ag) { |
| if (ag->device == device) |
| return ag->slc_handle; |
| } |
| return NULL; |
| } |
| |
| struct packet_status_logger *cras_hfp_ag_get_wbs_logger() |
| { |
| return &wbs_logger; |
| } |
| |
| int cras_hsp_ag_profile_create(DBusConnection *conn) |
| { |
| return cras_bt_add_profile(conn, &cras_hsp_ag_profile); |
| } |