| /* Copyright (c) 2014 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 <sys/time.h> |
| #include <syslog.h> |
| |
| #include "cras_bt_io.h" |
| #include "cras_bt_device.h" |
| #include "cras_hfp_iodev.h" |
| #include "cras_utf8.h" |
| #include "cras_iodev.h" |
| #include "cras_iodev_list.h" |
| #include "sfh.h" |
| #include "utlist.h" |
| |
| #define DEFAULT_BT_DEVICE_NAME "BLUETOOTH" |
| |
| /* Extends cras_ionode to hold bluetooth profile information |
| * so that iodevs of different profile(A2DP or HFP/HSP) can be |
| * associated with the same bt_io. |
| * Members: |
| * base - The base class cras_ionode. |
| * profile_dev - Pointer to the profile specific iodev. |
| * profile - The bluetooth profile profile_dev runs on. |
| */ |
| struct bt_node { |
| struct cras_ionode base; |
| struct cras_iodev *profile_dev; |
| unsigned int profile; |
| }; |
| |
| /* The structure represents a virtual input or output device of a |
| * bluetooth audio device, speaker or headset for example. A node |
| * will be added to this virtual iodev for each profile supported |
| * by the bluetooth audio device. |
| * Member: |
| * base - The base class cras_iodev |
| * next_node_id - The index will give to the next node |
| */ |
| struct bt_io { |
| struct cras_iodev base; |
| unsigned int next_node_id; |
| struct cras_bt_device *device; |
| }; |
| |
| /* Returns the active profile specific iodev. */ |
| static struct cras_iodev *active_profile_dev(const struct cras_iodev *iodev) |
| { |
| struct bt_node *active = (struct bt_node *)iodev->active_node; |
| |
| return active->profile_dev; |
| } |
| |
| /* Adds a profile specific iodev to btio. */ |
| static struct cras_ionode *add_profile_dev(struct cras_iodev *bt_iodev, |
| struct cras_iodev *dev, |
| enum cras_bt_device_profile profile) |
| { |
| struct bt_node *n; |
| struct bt_io *btio = (struct bt_io *)bt_iodev; |
| |
| n = (struct bt_node *)calloc(1, sizeof(*n)); |
| if (!n) |
| return NULL; |
| |
| n->base.dev = bt_iodev; |
| n->base.idx = btio->next_node_id++; |
| n->base.type = CRAS_NODE_TYPE_BLUETOOTH; |
| n->base.volume = 100; |
| n->base.stable_id = dev->info.stable_id; |
| n->base.capture_gain = 0; |
| gettimeofday(&n->base.plugged_time, NULL); |
| |
| strcpy(n->base.name, dev->info.name); |
| n->profile_dev = dev; |
| n->profile = profile; |
| |
| cras_iodev_add_node(bt_iodev, &n->base); |
| return &n->base; |
| } |
| |
| /* Forces bt device to switch to use the given profile. Note that if |
| * it has already been open for streaming, the new active profile will |
| * take effect after the related btio(s) are reopened. |
| */ |
| static void bt_switch_to_profile(struct cras_bt_device *device, |
| enum cras_bt_device_profile profile) |
| { |
| switch (profile) { |
| case CRAS_BT_DEVICE_PROFILE_HFP_AUDIOGATEWAY: |
| case CRAS_BT_DEVICE_PROFILE_HSP_AUDIOGATEWAY: |
| cras_bt_device_set_active_profile( |
| device, |
| CRAS_BT_DEVICE_PROFILE_HSP_AUDIOGATEWAY | |
| CRAS_BT_DEVICE_PROFILE_HFP_AUDIOGATEWAY); |
| break; |
| case CRAS_BT_DEVICE_PROFILE_A2DP_SOURCE: |
| cras_bt_device_set_active_profile( |
| device, CRAS_BT_DEVICE_PROFILE_A2DP_SOURCE); |
| break; |
| default: |
| syslog(LOG_ERR, "Unexpect profile %u", profile); |
| break; |
| } |
| } |
| |
| /* Switches the active profile to A2DP if it can. */ |
| static void bt_possibly_switch_to_a2dp(struct bt_io *btio) |
| { |
| if (!cras_bt_device_has_a2dp(btio->device)) |
| return; |
| cras_bt_device_set_active_profile(btio->device, |
| CRAS_BT_DEVICE_PROFILE_A2DP_SOURCE); |
| cras_bt_device_switch_profile(btio->device, &btio->base); |
| } |
| |
| /* Checks if bt device is active for the given profile. |
| */ |
| static int device_using_profile(struct cras_bt_device *device, |
| unsigned int profile) |
| { |
| return cras_bt_device_get_active_profile(device) & profile; |
| } |
| |
| /* Checks if the condition is met to switch to a different profile based |
| * on two rules: |
| * (1) Prefer to use A2DP for output since the audio quality is better. |
| * (2) Must use HFP/HSP for input since A2DP doesn't support audio input. |
| * |
| * If the profile switch happens, return non-zero error code, otherwise |
| * return zero. |
| */ |
| static int open_dev(struct cras_iodev *iodev) |
| { |
| struct bt_io *btio = (struct bt_io *)iodev; |
| struct cras_iodev *dev = active_profile_dev(iodev); |
| int rc; |
| |
| /* Force to use HFP if opening input dev. */ |
| if (device_using_profile(btio->device, |
| CRAS_BT_DEVICE_PROFILE_A2DP_SOURCE) && |
| iodev->direction == CRAS_STREAM_INPUT) { |
| bt_switch_to_profile(btio->device, |
| CRAS_BT_DEVICE_PROFILE_HFP_AUDIOGATEWAY); |
| cras_bt_device_switch_profile_enable_dev(btio->device, iodev); |
| return -EAGAIN; |
| } |
| |
| if (dev && dev->open_dev) { |
| rc = dev->open_dev(dev); |
| if (rc == 0) |
| return 0; |
| |
| /* If input iodev open fails, switch profile back to A2DP. */ |
| if (iodev->direction == CRAS_STREAM_INPUT) |
| bt_possibly_switch_to_a2dp(btio); |
| return rc; |
| } |
| |
| return 0; |
| } |
| |
| static int update_supported_formats(struct cras_iodev *iodev) |
| { |
| struct cras_iodev *dev = active_profile_dev(iodev); |
| int rc, length, i; |
| |
| if (!dev) |
| return -EINVAL; |
| |
| if (dev->update_supported_formats) { |
| rc = dev->update_supported_formats(dev); |
| if (rc) |
| return rc; |
| } |
| |
| /* Fill in the supported rates and channel counts. */ |
| for (length = 0; dev->supported_rates[length]; length++) |
| ; |
| free(iodev->supported_rates); |
| iodev->supported_rates = (size_t *)malloc( |
| (length + 1) * sizeof(*iodev->supported_rates)); |
| for (i = 0; i < length + 1; i++) |
| iodev->supported_rates[i] = dev->supported_rates[i]; |
| |
| for (length = 0; dev->supported_channel_counts[length]; length++) |
| ; |
| iodev->supported_channel_counts = (size_t *)malloc( |
| (length + 1) * sizeof(*iodev->supported_channel_counts)); |
| for (i = 0; i < length + 1; i++) |
| iodev->supported_channel_counts[i] = |
| dev->supported_channel_counts[i]; |
| |
| for (length = 0; dev->supported_formats[length]; length++) |
| ; |
| |
| iodev->supported_formats = (snd_pcm_format_t *)malloc( |
| (length + 1) * sizeof(*iodev->supported_formats)); |
| for (i = 0; i < length + 1; i++) |
| iodev->supported_formats[i] = dev->supported_formats[i]; |
| |
| /* Record max supported channels into cras_iodev_info. */ |
| iodev->info.max_supported_channels = dev->info.max_supported_channels; |
| return 0; |
| } |
| |
| static int configure_dev(struct cras_iodev *iodev) |
| { |
| int rc; |
| struct cras_iodev *dev = active_profile_dev(iodev); |
| if (!dev) |
| return -EINVAL; |
| |
| /* Fill back the format iodev is using. */ |
| if (dev->format == NULL) { |
| dev->format = (struct cras_audio_format *)malloc( |
| sizeof(*dev->format)); |
| if (!dev->format) |
| return -ENOMEM; |
| *dev->format = *iodev->format; |
| } |
| |
| rc = dev->configure_dev(dev); |
| if (rc) |
| return rc; |
| |
| iodev->buffer_size = dev->buffer_size; |
| iodev->min_buffer_level = dev->min_buffer_level; |
| if (dev->start) |
| dev->state = CRAS_IODEV_STATE_OPEN; |
| else |
| dev->state = CRAS_IODEV_STATE_NO_STREAM_RUN; |
| |
| return 0; |
| } |
| |
| static int close_dev(struct cras_iodev *iodev) |
| { |
| struct bt_io *btio = (struct bt_io *)iodev; |
| int rc; |
| struct cras_iodev *dev = active_profile_dev(iodev); |
| if (!dev) |
| return -EINVAL; |
| |
| /* If input iodev is in open state and being closed, switch profile |
| * from HFP to A2DP. */ |
| if (cras_iodev_is_open(iodev) && |
| device_using_profile( |
| btio->device, |
| CRAS_BT_DEVICE_PROFILE_HSP_AUDIOGATEWAY | |
| CRAS_BT_DEVICE_PROFILE_HFP_AUDIOGATEWAY) && |
| (iodev->direction == CRAS_STREAM_INPUT)) |
| bt_possibly_switch_to_a2dp(btio); |
| |
| rc = dev->close_dev(dev); |
| if (rc < 0) |
| return rc; |
| cras_iodev_free_format(iodev); |
| dev->state = CRAS_IODEV_STATE_CLOSE; |
| return 0; |
| } |
| |
| static void set_bt_volume(struct cras_iodev *iodev) |
| { |
| struct cras_iodev *dev = active_profile_dev(iodev); |
| if (!dev) |
| return; |
| |
| if (dev->active_node) |
| dev->active_node->volume = iodev->active_node->volume; |
| |
| /* The parent bt_iodev could set software_volume_needed flag for cases |
| * that software volume provides better experience across profiles |
| * (HFP and A2DP). Otherwise, use the profile specific implementation |
| * to adjust volume. */ |
| if (dev->set_volume && !iodev->software_volume_needed) |
| dev->set_volume(dev); |
| } |
| |
| static int frames_queued(const struct cras_iodev *iodev, |
| struct timespec *tstamp) |
| { |
| struct cras_iodev *dev = active_profile_dev(iodev); |
| if (!dev) |
| return -EINVAL; |
| return dev->frames_queued(dev, tstamp); |
| } |
| |
| static int delay_frames(const struct cras_iodev *iodev) |
| { |
| struct cras_iodev *dev = active_profile_dev(iodev); |
| if (!dev) |
| return -EINVAL; |
| return dev->delay_frames(dev); |
| } |
| |
| static int get_buffer(struct cras_iodev *iodev, struct cras_audio_area **area, |
| unsigned *frames) |
| { |
| struct cras_iodev *dev = active_profile_dev(iodev); |
| if (!dev) |
| return -EINVAL; |
| return dev->get_buffer(dev, area, frames); |
| } |
| |
| static int put_buffer(struct cras_iodev *iodev, unsigned nwritten) |
| { |
| struct cras_iodev *dev = active_profile_dev(iodev); |
| if (!dev) |
| return -EINVAL; |
| return dev->put_buffer(dev, nwritten); |
| } |
| |
| static int flush_buffer(struct cras_iodev *iodev) |
| { |
| struct cras_iodev *dev = active_profile_dev(iodev); |
| if (!dev) |
| return -EINVAL; |
| return dev->flush_buffer(dev); |
| } |
| |
| /* If the first private iodev doesn't match the active profile stored on |
| * device, select to the correct private iodev. |
| */ |
| static void update_active_node(struct cras_iodev *iodev, unsigned node_idx, |
| unsigned dev_enabled) |
| { |
| struct bt_io *btio = (struct bt_io *)iodev; |
| struct cras_ionode *node; |
| struct bt_node *active = (struct bt_node *)iodev->active_node; |
| struct cras_iodev *dev; |
| int rc; |
| |
| if (device_using_profile(btio->device, active->profile)) |
| goto leave; |
| |
| /* Switch to the correct dev using active_profile. */ |
| DL_FOREACH (iodev->nodes, node) { |
| struct bt_node *n = (struct bt_node *)node; |
| if (n == active) |
| continue; |
| |
| if (device_using_profile(btio->device, n->profile)) { |
| active->profile = n->profile; |
| active->profile_dev = n->profile_dev; |
| |
| /* Set volume for the new profile. */ |
| set_bt_volume(iodev); |
| } |
| } |
| |
| leave: |
| dev = active_profile_dev(iodev); |
| if (dev && dev->update_active_node) |
| dev->update_active_node(dev, node_idx, dev_enabled); |
| |
| /* Update supported formats here to get the supported formats from the |
| * new updated active profile dev. |
| */ |
| rc = update_supported_formats(iodev); |
| if (rc) { |
| syslog(LOG_ERR, "Failed to update supported formats, rc=%d", |
| rc); |
| } |
| } |
| |
| static int output_underrun(struct cras_iodev *iodev) |
| { |
| struct cras_iodev *dev = active_profile_dev(iodev); |
| if (!dev) |
| return -EINVAL; |
| |
| if (dev->output_underrun) { |
| dev->min_cb_level = iodev->min_cb_level; |
| dev->max_cb_level = iodev->max_cb_level; |
| dev->buffer_size = iodev->buffer_size; |
| return dev->output_underrun(dev); |
| } |
| |
| return 0; |
| } |
| |
| static int no_stream(struct cras_iodev *iodev, int enable) |
| { |
| struct cras_iodev *dev = active_profile_dev(iodev); |
| int rc; |
| |
| if (!dev) |
| return -EINVAL; |
| |
| if (dev->no_stream) { |
| /* |
| * Copy iodev->min_cb_level and iodev->max_cb_level from the |
| * parent (i.e. bt_io). no_stream() of hfp_alsa_iodev will |
| * use them. |
| * A2DP and HFP dev will use buffer and callback sizes to fill |
| * zeros in no stream state. |
| */ |
| dev->min_cb_level = iodev->min_cb_level; |
| dev->max_cb_level = iodev->max_cb_level; |
| dev->buffer_size = iodev->buffer_size; |
| rc = dev->no_stream(dev, enable); |
| if (rc < 0) |
| return rc; |
| } |
| if (enable) |
| dev->state = CRAS_IODEV_STATE_NO_STREAM_RUN; |
| else |
| dev->state = CRAS_IODEV_STATE_NORMAL_RUN; |
| |
| return 0; |
| } |
| |
| static int is_free_running(const struct cras_iodev *iodev) |
| { |
| struct cras_iodev *dev = active_profile_dev(iodev); |
| if (!dev) |
| return -EINVAL; |
| |
| if (dev->is_free_running) |
| return dev->is_free_running(dev); |
| |
| return 0; |
| } |
| |
| static int start(const struct cras_iodev *iodev) |
| { |
| struct cras_iodev *dev = active_profile_dev(iodev); |
| int rc; |
| |
| if (!dev) |
| return -EINVAL; |
| |
| if (dev->start) { |
| rc = dev->start(dev); |
| if (rc) |
| return rc; |
| } |
| dev->state = CRAS_IODEV_STATE_NORMAL_RUN; |
| return 0; |
| } |
| |
| static unsigned int frames_to_play_in_sleep(struct cras_iodev *iodev, |
| unsigned int *hw_level, |
| struct timespec *hw_tstamp) |
| { |
| struct cras_iodev *dev = active_profile_dev(iodev); |
| if (!dev || !dev->frames_to_play_in_sleep) |
| return cras_iodev_default_frames_to_play_in_sleep( |
| iodev, hw_level, hw_tstamp); |
| |
| return dev->frames_to_play_in_sleep(dev, hw_level, hw_tstamp); |
| } |
| |
| static int get_valid_frames(struct cras_iodev *iodev, |
| struct timespec *hw_tstamp) |
| { |
| struct cras_iodev *dev = active_profile_dev(iodev); |
| if (!dev) |
| return -EINVAL; |
| |
| if (dev->get_valid_frames) |
| return dev->get_valid_frames(dev, hw_tstamp); |
| |
| return cras_iodev_frames_queued(iodev, hw_tstamp); |
| } |
| |
| struct cras_iodev *cras_bt_io_create(struct cras_bt_device *device, |
| struct cras_iodev *dev, |
| enum cras_bt_device_profile profile) |
| { |
| int err; |
| struct bt_io *btio; |
| struct cras_iodev *iodev; |
| struct cras_ionode *node; |
| struct bt_node *active; |
| |
| if (!dev) |
| return NULL; |
| |
| btio = (struct bt_io *)calloc(1, sizeof(*btio)); |
| if (!btio) |
| goto error; |
| btio->device = device; |
| |
| iodev = &btio->base; |
| iodev->direction = dev->direction; |
| strcpy(iodev->info.name, dev->info.name); |
| iodev->info.stable_id = dev->info.stable_id; |
| |
| iodev->open_dev = open_dev; |
| iodev->configure_dev = configure_dev; |
| iodev->frames_queued = frames_queued; |
| iodev->delay_frames = delay_frames; |
| iodev->get_buffer = get_buffer; |
| iodev->put_buffer = put_buffer; |
| iodev->flush_buffer = flush_buffer; |
| iodev->close_dev = close_dev; |
| iodev->update_supported_formats = update_supported_formats; |
| iodev->update_active_node = update_active_node; |
| iodev->no_stream = no_stream; |
| iodev->output_underrun = output_underrun; |
| iodev->is_free_running = is_free_running; |
| iodev->get_valid_frames = get_valid_frames; |
| iodev->start = start; |
| iodev->frames_to_play_in_sleep = frames_to_play_in_sleep; |
| |
| /* Input also checks |software_volume_needed| flag for using software |
| * gain. Keep it as false for BT input. |
| * TODO(hychao): after wide band speech mode is supported, consider |
| * enable software gain. |
| */ |
| if (dev->direction == CRAS_STREAM_OUTPUT) { |
| iodev->software_volume_needed = |
| !cras_bt_device_get_use_hardware_volume(device); |
| iodev->set_volume = set_bt_volume; |
| } |
| |
| /* Create the fake node so it's the only node exposed to UI, and |
| * point it to the first profile dev. */ |
| active = (struct bt_node *)calloc(1, sizeof(*active)); |
| if (!active) |
| goto error; |
| active->base.dev = iodev; |
| active->base.idx = btio->next_node_id++; |
| active->base.type = dev->active_node->type; |
| active->base.volume = 100; |
| active->base.stable_id = cras_bt_device_get_stable_id(device); |
| active->base.ui_gain_scaler = 1.0f; |
| /* |
| * If the same headset is connected in wideband mode, we shall assign |
| * a separate stable_id so the node priority/preference mechanism in |
| * Chrome UI doesn't break. |
| */ |
| if ((active->base.type == CRAS_NODE_TYPE_BLUETOOTH) && |
| (dev->direction == CRAS_STREAM_INPUT)) |
| active->base.stable_id = |
| SuperFastHash((const char *)&active->base.type, |
| sizeof(active->base.type), |
| active->base.stable_id); |
| active->profile = profile; |
| active->profile_dev = dev; |
| strcpy(active->base.name, dev->info.name); |
| /* The node name exposed to UI should be a valid UTF8 string. */ |
| if (!is_utf8_string(active->base.name)) |
| strcpy(active->base.name, DEFAULT_BT_DEVICE_NAME); |
| cras_iodev_add_node(iodev, &active->base); |
| |
| node = add_profile_dev(&btio->base, dev, profile); |
| if (node == NULL) |
| goto error; |
| |
| /* Default active profile to a2dp whenever it's allowed. */ |
| if (!cras_bt_device_get_active_profile(device) || |
| (profile == CRAS_BT_DEVICE_PROFILE_A2DP_SOURCE && |
| cras_bt_device_can_switch_to_a2dp(device))) |
| bt_switch_to_profile(device, profile); |
| |
| if (iodev->direction == CRAS_STREAM_OUTPUT) |
| err = cras_iodev_list_add_output(iodev); |
| else |
| err = cras_iodev_list_add_input(iodev); |
| if (err) |
| goto error; |
| |
| cras_iodev_set_active_node(iodev, &active->base); |
| return &btio->base; |
| |
| error: |
| if (btio) |
| free(btio); |
| return NULL; |
| } |
| |
| void cras_bt_io_free_resources(struct cras_iodev *bt_iodev) |
| { |
| struct cras_ionode *node; |
| struct bt_node *n; |
| |
| free(bt_iodev->supported_rates); |
| free(bt_iodev->supported_channel_counts); |
| free(bt_iodev->supported_formats); |
| |
| DL_FOREACH (bt_iodev->nodes, node) { |
| n = (struct bt_node *)node; |
| cras_iodev_rm_node(bt_iodev, node); |
| free(n); |
| } |
| |
| cras_iodev_free_resources(bt_iodev); |
| } |
| |
| void cras_bt_io_destroy(struct cras_iodev *bt_iodev) |
| { |
| int rc; |
| struct bt_io *btio = (struct bt_io *)bt_iodev; |
| |
| if (bt_iodev->direction == CRAS_STREAM_OUTPUT) |
| rc = cras_iodev_list_rm_output(bt_iodev); |
| else |
| rc = cras_iodev_list_rm_input(bt_iodev); |
| if (rc == -EBUSY) |
| return; |
| |
| cras_bt_io_free_resources(bt_iodev); |
| free(btio); |
| } |
| |
| struct cras_ionode *cras_bt_io_get_profile(struct cras_iodev *bt_iodev, |
| enum cras_bt_device_profile profile) |
| { |
| struct cras_ionode *node; |
| DL_FOREACH (bt_iodev->nodes, node) { |
| struct bt_node *n = (struct bt_node *)node; |
| if (n->profile & profile) |
| return node; |
| } |
| return NULL; |
| } |
| |
| int cras_bt_io_append(struct cras_iodev *bt_iodev, struct cras_iodev *dev, |
| enum cras_bt_device_profile profile) |
| { |
| struct cras_ionode *node; |
| struct bt_io *btio = (struct bt_io *)bt_iodev; |
| |
| if (cras_bt_io_get_profile(bt_iodev, profile)) |
| return -EEXIST; |
| |
| node = add_profile_dev(bt_iodev, dev, profile); |
| if (!node) |
| return -ENOMEM; |
| |
| if (profile == CRAS_BT_DEVICE_PROFILE_A2DP_SOURCE && |
| cras_bt_device_can_switch_to_a2dp(btio->device)) { |
| bt_switch_to_profile(btio->device, |
| CRAS_BT_DEVICE_PROFILE_A2DP_SOURCE); |
| cras_bt_device_switch_profile(btio->device, bt_iodev); |
| syslog(LOG_ERR, "Switch to A2DP on append"); |
| } |
| return 0; |
| } |
| |
| int cras_bt_io_on_profile(struct cras_iodev *bt_iodev, |
| enum cras_bt_device_profile profile) |
| { |
| struct bt_node *btnode = (struct bt_node *)bt_iodev->active_node; |
| return !!(profile & btnode->profile); |
| } |
| |
| enum cras_bt_device_profile |
| cras_bt_io_profile_to_log(struct cras_iodev *bt_iodev) |
| { |
| struct bt_node *btnode = (struct bt_node *)bt_iodev->active_node; |
| |
| if (btnode->profile & CRAS_BT_DEVICE_PROFILE_A2DP_SOURCE) |
| return CRAS_BT_DEVICE_PROFILE_A2DP_SOURCE; |
| |
| if (hfp_iodev_is_hsp(btnode->profile_dev)) |
| return CRAS_BT_DEVICE_PROFILE_HSP_AUDIOGATEWAY; |
| else |
| return CRAS_BT_DEVICE_PROFILE_HFP_AUDIOGATEWAY; |
| } |
| |
| unsigned int cras_bt_io_try_remove(struct cras_iodev *bt_iodev, |
| struct cras_iodev *dev) |
| { |
| struct cras_ionode *node; |
| struct bt_node *active, *btnode; |
| unsigned int try_profile = 0; |
| |
| active = (struct bt_node *)bt_iodev->active_node; |
| |
| if (active->profile_dev == dev) { |
| DL_FOREACH (bt_iodev->nodes, node) { |
| btnode = (struct bt_node *)node; |
| /* Skip the active node and the node we're trying |
| * to remove. */ |
| if (btnode == active || btnode->profile_dev == dev) |
| continue; |
| try_profile = btnode->profile; |
| break; |
| } |
| } else { |
| try_profile = active->profile; |
| } |
| return try_profile; |
| } |
| |
| int cras_bt_io_remove(struct cras_iodev *bt_iodev, struct cras_iodev *dev) |
| { |
| struct cras_ionode *node; |
| struct bt_node *btnode; |
| |
| DL_FOREACH (bt_iodev->nodes, node) { |
| btnode = (struct bt_node *)node; |
| if (btnode->profile_dev != dev) |
| continue; |
| |
| /* If this is the active node, reset it. Otherwise delete |
| * this node. */ |
| if (node == bt_iodev->active_node) { |
| btnode->profile_dev = NULL; |
| btnode->profile = 0; |
| } else { |
| DL_DELETE(bt_iodev->nodes, node); |
| free(node); |
| } |
| } |
| |
| /* The node of active profile could have been removed, update it. |
| * Return err when fail to locate the active profile dev. */ |
| update_active_node(bt_iodev, 0, 1); |
| btnode = (struct bt_node *)bt_iodev->active_node; |
| if ((btnode->profile == 0) || (btnode->profile_dev == NULL)) |
| return -EINVAL; |
| |
| return 0; |
| } |