| // SPDX-License-Identifier: LGPL-2.1-or-later |
| /* |
| * |
| * BlueZ - Bluetooth protocol stack for Linux |
| * |
| * Copyright (C) 2012-2014 Intel Corporation. All rights reserved. |
| * |
| * |
| */ |
| |
| #ifdef HAVE_CONFIG_H |
| #include <config.h> |
| #endif |
| |
| #define _GNU_SOURCE |
| #include <stdio.h> |
| #include <unistd.h> |
| #include <string.h> |
| #include <stdarg.h> |
| #include <ctype.h> |
| #include <limits.h> |
| |
| #include "src/shared/util.h" |
| #include "src/shared/ringbuf.h" |
| #include "src/shared/queue.h" |
| #include "src/shared/io.h" |
| #include "src/shared/hfp.h" |
| |
| #define DBG(_hfp, fmt, arg...) \ |
| hfp_debug(_hfp->debug_callback, _hfp->debug_data, "%s:%s() " fmt, \ |
| __FILE__, __func__, ## arg) |
| |
| #define HFP_HF_FEATURES ( \ |
| HFP_HF_FEAT_ECNR | \ |
| HFP_HF_FEAT_3WAY | \ |
| HFP_HF_FEAT_CLIP | \ |
| HFP_HF_FEAT_ENHANCED_CALL_STATUS | \ |
| HFP_HF_FEAT_ESCO_S4_T2 \ |
| ) |
| |
| struct hfp_gw { |
| int ref_count; |
| int fd; |
| bool close_on_unref; |
| struct io *io; |
| struct ringbuf *read_buf; |
| struct ringbuf *write_buf; |
| struct queue *cmd_handlers; |
| bool writer_active; |
| bool result_pending; |
| hfp_command_func_t command_callback; |
| hfp_destroy_func_t command_destroy; |
| void *command_data; |
| hfp_debug_func_t debug_callback; |
| hfp_destroy_func_t debug_destroy; |
| void *debug_data; |
| |
| hfp_disconnect_func_t disconnect_callback; |
| hfp_destroy_func_t disconnect_destroy; |
| void *disconnect_data; |
| |
| bool in_disconnect; |
| bool destroyed; |
| }; |
| |
| typedef void (*ciev_func_t)(uint8_t val, void *user_data); |
| |
| struct indicator { |
| uint8_t index; |
| uint32_t min; |
| uint32_t max; |
| uint32_t val; |
| ciev_func_t cb; |
| }; |
| |
| struct hfp_hf { |
| int ref_count; |
| int fd; |
| bool close_on_unref; |
| struct io *io; |
| struct ringbuf *read_buf; |
| struct ringbuf *write_buf; |
| |
| bool writer_active; |
| struct queue *cmd_queue; |
| |
| struct queue *event_handlers; |
| |
| hfp_debug_func_t debug_callback; |
| hfp_destroy_func_t debug_destroy; |
| void *debug_data; |
| |
| hfp_disconnect_func_t disconnect_callback; |
| hfp_destroy_func_t disconnect_destroy; |
| void *disconnect_data; |
| |
| bool in_disconnect; |
| bool destroyed; |
| |
| struct hfp_hf_callbacks *callbacks; |
| void *callbacks_data; |
| |
| uint32_t features; |
| struct indicator ag_ind[HFP_INDICATOR_LAST]; |
| bool service; |
| uint8_t signal; |
| bool roaming; |
| uint8_t battchg; |
| uint8_t chlds; |
| |
| bool session; |
| bool clcc_in_progress; |
| |
| struct queue *calls; |
| struct queue *updated_calls; |
| char *dialing_number; |
| }; |
| |
| struct cmd_handler { |
| char *prefix; |
| void *user_data; |
| hfp_destroy_func_t destroy; |
| hfp_result_func_t callback; |
| }; |
| |
| struct hfp_context { |
| const char *data; |
| unsigned int offset; |
| }; |
| |
| struct cmd_response { |
| hfp_response_func_t resp_cb; |
| struct hfp_context *response; |
| char *resp_data; |
| void *user_data; |
| }; |
| |
| struct event_handler { |
| char *prefix; |
| void *user_data; |
| hfp_destroy_func_t destroy; |
| hfp_hf_result_func_t callback; |
| }; |
| |
| struct hf_call { |
| uint id; |
| enum hfp_call_status status; |
| char *line_id; |
| uint type; |
| bool mpty; |
| |
| struct hfp_hf *hfp; |
| }; |
| |
| static void hfp_debug(hfp_debug_func_t debug_func, void *debug_data, |
| const char *format, ...) |
| { |
| va_list ap; |
| |
| if (!debug_func || !format) |
| return; |
| |
| va_start(ap, format); |
| util_debug_va(debug_func, debug_data, format, ap); |
| va_end(ap); |
| } |
| |
| static void destroy_cmd_handler(void *data) |
| { |
| struct cmd_handler *handler = data; |
| |
| if (handler->destroy) |
| handler->destroy(handler->user_data); |
| |
| free(handler->prefix); |
| |
| free(handler); |
| } |
| |
| static bool match_handler_prefix(const void *a, const void *b) |
| { |
| const struct cmd_handler *handler = a; |
| const char *prefix = b; |
| |
| if (strcmp(handler->prefix, prefix) != 0) |
| return false; |
| |
| return true; |
| } |
| |
| static void write_watch_destroy(void *user_data) |
| { |
| struct hfp_gw *hfp = user_data; |
| |
| hfp->writer_active = false; |
| } |
| |
| static bool can_write_data(struct io *io, void *user_data) |
| { |
| struct hfp_gw *hfp = user_data; |
| ssize_t bytes_written; |
| |
| bytes_written = ringbuf_write(hfp->write_buf, hfp->fd); |
| if (bytes_written < 0) |
| return false; |
| |
| if (ringbuf_len(hfp->write_buf) > 0) |
| return true; |
| |
| return false; |
| } |
| |
| static void wakeup_writer(struct hfp_gw *hfp) |
| { |
| if (hfp->writer_active) |
| return; |
| |
| if (!ringbuf_len(hfp->write_buf)) |
| return; |
| |
| if (!io_set_write_handler(hfp->io, can_write_data, |
| hfp, write_watch_destroy)) |
| return; |
| |
| hfp->writer_active = true; |
| } |
| |
| static void skip_whitespace(struct hfp_context *context) |
| { |
| while (context->data[context->offset] == ' ') |
| context->offset++; |
| } |
| |
| static void handle_unknown_at_command(struct hfp_gw *hfp, |
| const char *data) |
| { |
| if (hfp->command_callback) { |
| hfp->result_pending = true; |
| hfp->command_callback(data, hfp->command_data); |
| } else { |
| hfp_gw_send_result(hfp, HFP_RESULT_ERROR); |
| } |
| } |
| |
| static bool handle_at_command(struct hfp_gw *hfp, const char *data) |
| { |
| struct cmd_handler *handler; |
| const char *separators = ";?=\0"; |
| struct hfp_context context; |
| enum hfp_gw_cmd_type type; |
| char lookup_prefix[18]; |
| uint8_t pref_len = 0; |
| const char *prefix; |
| int i; |
| |
| context.offset = 0; |
| context.data = data; |
| |
| skip_whitespace(&context); |
| |
| if (strlen(data + context.offset) < 3) |
| return false; |
| |
| if (strncmp(data + context.offset, "AT", 2)) |
| if (strncmp(data + context.offset, "at", 2)) |
| return false; |
| |
| context.offset += 2; |
| prefix = data + context.offset; |
| |
| if (isalpha(prefix[0])) { |
| lookup_prefix[pref_len++] = toupper(prefix[0]); |
| } else { |
| pref_len = strcspn(prefix, separators); |
| if (pref_len > 17 || pref_len < 2) |
| return false; |
| |
| for (i = 0; i < pref_len; i++) |
| lookup_prefix[i] = toupper(prefix[i]); |
| } |
| |
| lookup_prefix[pref_len] = '\0'; |
| context.offset += pref_len; |
| |
| if (lookup_prefix[0] == 'D') { |
| type = HFP_GW_CMD_TYPE_SET; |
| goto done; |
| } |
| |
| if (data[context.offset] == '=') { |
| context.offset++; |
| if (data[context.offset] == '?') { |
| context.offset++; |
| type = HFP_GW_CMD_TYPE_TEST; |
| } else { |
| type = HFP_GW_CMD_TYPE_SET; |
| } |
| goto done; |
| } |
| |
| if (data[context.offset] == '?') { |
| context.offset++; |
| type = HFP_GW_CMD_TYPE_READ; |
| goto done; |
| } |
| |
| type = HFP_GW_CMD_TYPE_COMMAND; |
| |
| done: |
| |
| handler = queue_find(hfp->cmd_handlers, match_handler_prefix, |
| lookup_prefix); |
| if (!handler) { |
| handle_unknown_at_command(hfp, data); |
| return true; |
| } |
| |
| hfp->result_pending = true; |
| handler->callback(&context, type, handler->user_data); |
| |
| return true; |
| } |
| |
| static void next_field(struct hfp_context *context) |
| { |
| if (context->data[context->offset] == ',') |
| context->offset++; |
| } |
| |
| bool hfp_context_get_number_default(struct hfp_context *context, |
| unsigned int *val, |
| unsigned int default_val) |
| { |
| skip_whitespace(context); |
| |
| if (context->data[context->offset] == ',') { |
| if (val) |
| *val = default_val; |
| |
| context->offset++; |
| return true; |
| } |
| |
| return hfp_context_get_number(context, val); |
| } |
| |
| bool hfp_context_get_number(struct hfp_context *context, |
| unsigned int *val) |
| { |
| unsigned int i; |
| int tmp = 0; |
| |
| skip_whitespace(context); |
| |
| i = context->offset; |
| |
| while (context->data[i] >= '0' && context->data[i] <= '9') |
| tmp = tmp * 10 + context->data[i++] - '0'; |
| |
| if (i == context->offset) |
| return false; |
| |
| if (val) |
| *val = tmp; |
| context->offset = i; |
| |
| skip_whitespace(context); |
| next_field(context); |
| |
| return true; |
| } |
| |
| bool hfp_context_open_container(struct hfp_context *context) |
| { |
| skip_whitespace(context); |
| |
| /* The list shall be preceded by a left parenthesis "(") */ |
| if (context->data[context->offset] != '(') |
| return false; |
| |
| context->offset++; |
| |
| return true; |
| } |
| |
| bool hfp_context_close_container(struct hfp_context *context) |
| { |
| skip_whitespace(context); |
| |
| /* The list shall be followed by a right parenthesis (")" V250 5.7.3.1*/ |
| if (context->data[context->offset] != ')') |
| return false; |
| |
| context->offset++; |
| |
| next_field(context); |
| |
| return true; |
| } |
| |
| bool hfp_context_is_container_close(struct hfp_context *context) |
| { |
| return context->data[context->offset] == ')'; |
| } |
| |
| |
| bool hfp_context_get_string(struct hfp_context *context, char *buf, |
| uint8_t len) |
| { |
| int i = 0; |
| const char *data = context->data; |
| unsigned int offset; |
| |
| skip_whitespace(context); |
| |
| if (data[context->offset] != '"') |
| return false; |
| |
| offset = context->offset; |
| offset++; |
| |
| while (data[offset] != '\0' && data[offset] != '"') { |
| if (i == len) |
| return false; |
| |
| buf[i++] = data[offset]; |
| offset++; |
| } |
| |
| if (i == len) |
| return false; |
| |
| buf[i] = '\0'; |
| |
| if (data[offset] == '"') |
| offset++; |
| else |
| return false; |
| |
| context->offset = offset; |
| |
| skip_whitespace(context); |
| next_field(context); |
| |
| return true; |
| } |
| |
| bool hfp_context_get_unquoted_string(struct hfp_context *context, |
| char *buf, uint8_t len) |
| { |
| const char *data = context->data; |
| unsigned int offset; |
| int i = 0; |
| char c; |
| |
| skip_whitespace(context); |
| |
| c = data[context->offset]; |
| if (c == '"' || c == ')' || c == '(') |
| return false; |
| |
| offset = context->offset; |
| |
| while (data[offset] != '\0' && data[offset] != ',' && |
| data[offset] != ')') { |
| if (i == len) |
| return false; |
| |
| buf[i++] = data[offset]; |
| offset++; |
| } |
| |
| if (i == len) |
| return false; |
| |
| buf[i] = '\0'; |
| |
| context->offset = offset; |
| |
| next_field(context); |
| |
| return true; |
| } |
| |
| bool hfp_context_has_next(struct hfp_context *context) |
| { |
| return context->data[context->offset] != '\0'; |
| } |
| |
| void hfp_context_skip_field(struct hfp_context *context) |
| { |
| const char *data = context->data; |
| unsigned int offset = context->offset; |
| |
| while (data[offset] != '\0' && data[offset] != ',') |
| offset++; |
| |
| context->offset = offset; |
| next_field(context); |
| } |
| |
| bool hfp_context_get_range(struct hfp_context *context, uint32_t *min, |
| uint32_t *max) |
| { |
| uint32_t l, h; |
| uint32_t start; |
| |
| start = context->offset; |
| |
| if (!hfp_context_get_number(context, &l)) |
| goto failed; |
| |
| if (context->data[context->offset] != '-') |
| goto failed; |
| |
| context->offset++; |
| |
| if (!hfp_context_get_number(context, &h)) |
| goto failed; |
| |
| *min = l; |
| *max = h; |
| |
| next_field(context); |
| |
| return true; |
| |
| failed: |
| context->offset = start; |
| return false; |
| } |
| |
| static void process_input(struct hfp_gw *hfp) |
| { |
| char *str, *ptr; |
| size_t len, count; |
| bool free_ptr = false; |
| bool read_again; |
| |
| do { |
| str = ringbuf_peek(hfp->read_buf, 0, &len); |
| if (!str) |
| return; |
| |
| ptr = memchr(str, '\r', len); |
| if (!ptr) { |
| char *str2; |
| size_t len2; |
| |
| /* |
| * If there is no more data in ringbuffer, |
| * it's just an incomplete command. |
| */ |
| if (len == ringbuf_len(hfp->read_buf)) |
| return; |
| |
| str2 = ringbuf_peek(hfp->read_buf, len, &len2); |
| if (!str2) |
| return; |
| |
| ptr = memchr(str2, '\r', len2); |
| if (!ptr) |
| return; |
| |
| *ptr = '\0'; |
| |
| count = len2 + len; |
| ptr = malloc(count); |
| if (!ptr) |
| return; |
| |
| memcpy(ptr, str, len); |
| memcpy(ptr + len, str2, len2); |
| |
| free_ptr = true; |
| str = ptr; |
| } else { |
| count = ptr - str; |
| *ptr = '\0'; |
| } |
| |
| if (!handle_at_command(hfp, str)) |
| /* |
| * Command is not handled that means that was some |
| * trash. Let's skip that and keep reading from ring |
| * buffer. |
| */ |
| read_again = true; |
| else |
| /* |
| * Command has been handled. If we are waiting for a |
| * result from upper layer, we can stop reading. If we |
| * already reply i.e. ERROR on unknown command, then we |
| * can keep reading ring buffer. Actually ring buffer |
| * should be empty but lets just look there. |
| */ |
| read_again = !hfp->result_pending; |
| |
| ringbuf_drain(hfp->read_buf, count + 1); |
| |
| if (free_ptr) |
| free(ptr); |
| |
| } while (read_again); |
| } |
| |
| static void read_watch_destroy(void *user_data) |
| { |
| } |
| |
| static bool can_read_data(struct io *io, void *user_data) |
| { |
| struct hfp_gw *hfp = user_data; |
| ssize_t bytes_read; |
| |
| bytes_read = ringbuf_read(hfp->read_buf, hfp->fd); |
| if (bytes_read < 0) |
| return false; |
| |
| if (hfp->result_pending) |
| return true; |
| |
| process_input(hfp); |
| |
| return true; |
| } |
| |
| struct hfp_gw *hfp_gw_new(int fd) |
| { |
| struct hfp_gw *hfp; |
| |
| if (fd < 0) |
| return NULL; |
| |
| hfp = new0(struct hfp_gw, 1); |
| hfp->fd = fd; |
| hfp->close_on_unref = false; |
| |
| hfp->read_buf = ringbuf_new(4096); |
| if (!hfp->read_buf) { |
| free(hfp); |
| return NULL; |
| } |
| |
| hfp->write_buf = ringbuf_new(4096); |
| if (!hfp->write_buf) { |
| ringbuf_free(hfp->read_buf); |
| free(hfp); |
| return NULL; |
| } |
| |
| hfp->io = io_new(fd); |
| if (!hfp->io) { |
| ringbuf_free(hfp->write_buf); |
| ringbuf_free(hfp->read_buf); |
| free(hfp); |
| return NULL; |
| } |
| |
| hfp->cmd_handlers = queue_new(); |
| |
| if (!io_set_read_handler(hfp->io, can_read_data, hfp, |
| read_watch_destroy)) { |
| queue_destroy(hfp->cmd_handlers, destroy_cmd_handler); |
| io_destroy(hfp->io); |
| ringbuf_free(hfp->write_buf); |
| ringbuf_free(hfp->read_buf); |
| free(hfp); |
| return NULL; |
| } |
| |
| hfp->writer_active = false; |
| hfp->result_pending = false; |
| |
| return hfp_gw_ref(hfp); |
| } |
| |
| struct hfp_gw *hfp_gw_ref(struct hfp_gw *hfp) |
| { |
| if (!hfp) |
| return NULL; |
| |
| __sync_fetch_and_add(&hfp->ref_count, 1); |
| |
| return hfp; |
| } |
| |
| void hfp_gw_unref(struct hfp_gw *hfp) |
| { |
| if (!hfp) |
| return; |
| |
| if (__sync_sub_and_fetch(&hfp->ref_count, 1)) |
| return; |
| |
| hfp_gw_set_command_handler(hfp, NULL, NULL, NULL); |
| |
| io_set_write_handler(hfp->io, NULL, NULL, NULL); |
| io_set_read_handler(hfp->io, NULL, NULL, NULL); |
| io_set_disconnect_handler(hfp->io, NULL, NULL, NULL); |
| |
| io_destroy(hfp->io); |
| hfp->io = NULL; |
| |
| if (hfp->close_on_unref) |
| close(hfp->fd); |
| |
| hfp_gw_set_debug(hfp, NULL, NULL, NULL); |
| |
| ringbuf_free(hfp->read_buf); |
| hfp->read_buf = NULL; |
| |
| ringbuf_free(hfp->write_buf); |
| hfp->write_buf = NULL; |
| |
| queue_destroy(hfp->cmd_handlers, destroy_cmd_handler); |
| hfp->cmd_handlers = NULL; |
| |
| if (!hfp->in_disconnect) { |
| free(hfp); |
| return; |
| } |
| |
| hfp->destroyed = true; |
| } |
| |
| static void read_tracing(const void *buf, size_t count, void *user_data) |
| { |
| struct hfp_gw *hfp = user_data; |
| |
| util_hexdump('>', buf, count, hfp->debug_callback, hfp->debug_data); |
| } |
| |
| static void write_tracing(const void *buf, size_t count, void *user_data) |
| { |
| struct hfp_gw *hfp = user_data; |
| |
| util_hexdump('<', buf, count, hfp->debug_callback, hfp->debug_data); |
| } |
| |
| bool hfp_gw_set_debug(struct hfp_gw *hfp, hfp_debug_func_t callback, |
| void *user_data, hfp_destroy_func_t destroy) |
| { |
| if (!hfp) |
| return false; |
| |
| if (hfp->debug_destroy) |
| hfp->debug_destroy(hfp->debug_data); |
| |
| hfp->debug_callback = callback; |
| hfp->debug_destroy = destroy; |
| hfp->debug_data = user_data; |
| |
| if (hfp->debug_callback) { |
| ringbuf_set_input_tracing(hfp->read_buf, read_tracing, hfp); |
| ringbuf_set_input_tracing(hfp->write_buf, write_tracing, hfp); |
| } else { |
| ringbuf_set_input_tracing(hfp->read_buf, NULL, NULL); |
| ringbuf_set_input_tracing(hfp->write_buf, NULL, NULL); |
| } |
| |
| return true; |
| } |
| |
| bool hfp_gw_set_close_on_unref(struct hfp_gw *hfp, bool do_close) |
| { |
| if (!hfp) |
| return false; |
| |
| hfp->close_on_unref = do_close; |
| |
| return true; |
| } |
| |
| bool hfp_gw_send_result(struct hfp_gw *hfp, enum hfp_result result) |
| { |
| const char *str; |
| |
| if (!hfp) |
| return false; |
| |
| switch (result) { |
| case HFP_RESULT_OK: |
| str = "OK"; |
| break; |
| case HFP_RESULT_ERROR: |
| str = "ERROR"; |
| break; |
| case HFP_RESULT_RING: |
| case HFP_RESULT_NO_CARRIER: |
| case HFP_RESULT_BUSY: |
| case HFP_RESULT_NO_ANSWER: |
| case HFP_RESULT_DELAYED: |
| case HFP_RESULT_REJECTED: |
| case HFP_RESULT_CME_ERROR: |
| case HFP_RESULT_NO_DIALTONE: |
| case HFP_RESULT_CONNECT: |
| default: |
| return false; |
| } |
| |
| if (ringbuf_printf(hfp->write_buf, "\r\n%s\r\n", str) < 0) |
| return false; |
| |
| wakeup_writer(hfp); |
| |
| /* |
| * There might be already something to read in the ring buffer. |
| * If so, let's read it. |
| */ |
| if (hfp->result_pending) { |
| hfp->result_pending = false; |
| process_input(hfp); |
| } |
| |
| return true; |
| } |
| |
| bool hfp_gw_send_error(struct hfp_gw *hfp, enum hfp_error error) |
| { |
| if (!hfp) |
| return false; |
| |
| if (ringbuf_printf(hfp->write_buf, "\r\n+CME ERROR: %u\r\n", error) < 0) |
| return false; |
| |
| wakeup_writer(hfp); |
| |
| /* |
| * There might be already something to read in the ring buffer. |
| * If so, let's read it. |
| */ |
| if (hfp->result_pending) { |
| hfp->result_pending = false; |
| process_input(hfp); |
| } |
| |
| return true; |
| } |
| |
| bool hfp_gw_send_info(struct hfp_gw *hfp, const char *format, ...) |
| { |
| va_list ap; |
| char *fmt; |
| int len; |
| |
| if (!hfp || !format) |
| return false; |
| |
| if (asprintf(&fmt, "\r\n%s\r\n", format) < 0) |
| return false; |
| |
| va_start(ap, format); |
| len = ringbuf_vprintf(hfp->write_buf, fmt, ap); |
| va_end(ap); |
| |
| free(fmt); |
| |
| if (len < 0) |
| return false; |
| |
| if (hfp->result_pending) |
| return true; |
| |
| wakeup_writer(hfp); |
| |
| return true; |
| } |
| |
| bool hfp_gw_set_command_handler(struct hfp_gw *hfp, |
| hfp_command_func_t callback, |
| void *user_data, hfp_destroy_func_t destroy) |
| { |
| if (!hfp) |
| return false; |
| |
| if (hfp->command_destroy) |
| hfp->command_destroy(hfp->command_data); |
| |
| hfp->command_callback = callback; |
| hfp->command_destroy = destroy; |
| hfp->command_data = user_data; |
| |
| return true; |
| } |
| |
| bool hfp_gw_register(struct hfp_gw *hfp, hfp_result_func_t callback, |
| const char *prefix, |
| void *user_data, |
| hfp_destroy_func_t destroy) |
| { |
| struct cmd_handler *handler; |
| |
| handler = new0(struct cmd_handler, 1); |
| handler->callback = callback; |
| handler->user_data = user_data; |
| |
| handler->prefix = strdup(prefix); |
| if (!handler->prefix) { |
| free(handler); |
| return false; |
| } |
| |
| if (queue_find(hfp->cmd_handlers, match_handler_prefix, |
| handler->prefix)) { |
| destroy_cmd_handler(handler); |
| return false; |
| } |
| |
| handler->destroy = destroy; |
| |
| return queue_push_tail(hfp->cmd_handlers, handler); |
| } |
| |
| bool hfp_gw_unregister(struct hfp_gw *hfp, const char *prefix) |
| { |
| struct cmd_handler *handler; |
| char *lookup_prefix; |
| |
| lookup_prefix = strdup(prefix); |
| if (!lookup_prefix) |
| return false; |
| |
| handler = queue_remove_if(hfp->cmd_handlers, match_handler_prefix, |
| lookup_prefix); |
| free(lookup_prefix); |
| |
| if (!handler) |
| return false; |
| |
| destroy_cmd_handler(handler); |
| |
| return true; |
| } |
| |
| static void disconnect_watch_destroy(void *user_data) |
| { |
| struct hfp_gw *hfp = user_data; |
| |
| if (hfp->disconnect_destroy) |
| hfp->disconnect_destroy(hfp->disconnect_data); |
| |
| if (hfp->destroyed) |
| free(hfp); |
| } |
| |
| static bool io_disconnected(struct io *io, void *user_data) |
| { |
| struct hfp_gw *hfp = user_data; |
| |
| hfp->in_disconnect = true; |
| |
| if (hfp->disconnect_callback) |
| hfp->disconnect_callback(hfp->disconnect_data); |
| |
| hfp->in_disconnect = false; |
| |
| return false; |
| } |
| |
| bool hfp_gw_set_disconnect_handler(struct hfp_gw *hfp, |
| hfp_disconnect_func_t callback, |
| void *user_data, |
| hfp_destroy_func_t destroy) |
| { |
| if (!hfp) |
| return false; |
| |
| if (hfp->disconnect_destroy) |
| hfp->disconnect_destroy(hfp->disconnect_data); |
| |
| if (!io_set_disconnect_handler(hfp->io, io_disconnected, hfp, |
| disconnect_watch_destroy)) { |
| hfp->disconnect_callback = NULL; |
| hfp->disconnect_destroy = NULL; |
| hfp->disconnect_data = NULL; |
| return false; |
| } |
| |
| hfp->disconnect_callback = callback; |
| hfp->disconnect_destroy = destroy; |
| hfp->disconnect_data = user_data; |
| |
| return true; |
| } |
| |
| bool hfp_gw_disconnect(struct hfp_gw *hfp) |
| { |
| if (!hfp) |
| return false; |
| |
| return io_shutdown(hfp->io); |
| } |
| |
| static bool match_handler_event_prefix(const void *a, const void *b) |
| { |
| const struct event_handler *handler = a; |
| const char *prefix = b; |
| |
| if (strcmp(handler->prefix, prefix) != 0) |
| return false; |
| |
| return true; |
| } |
| |
| static void destroy_event_handler(void *data) |
| { |
| struct event_handler *handler = data; |
| |
| if (handler->destroy) |
| handler->destroy(handler->user_data); |
| |
| free(handler->prefix); |
| |
| free(handler); |
| } |
| |
| static bool hf_can_write_data(struct io *io, void *user_data) |
| { |
| struct hfp_hf *hfp = user_data; |
| ssize_t bytes_written; |
| |
| bytes_written = ringbuf_write(hfp->write_buf, hfp->fd); |
| if (bytes_written < 0) |
| return false; |
| |
| if (ringbuf_len(hfp->write_buf) > 0) |
| return true; |
| |
| return false; |
| } |
| |
| static void hf_write_watch_destroy(void *user_data) |
| { |
| struct hfp_hf *hfp = user_data; |
| |
| hfp->writer_active = false; |
| } |
| |
| static void hf_skip_whitespace(struct hfp_context *context) |
| { |
| while (context->data[context->offset] == ' ') |
| context->offset++; |
| } |
| |
| static bool is_response(const char *prefix, enum hfp_result *result, |
| enum hfp_error *cme_err, |
| struct hfp_context *context) |
| { |
| if (strcmp(prefix, "OK") == 0) { |
| *result = HFP_RESULT_OK; |
| /* |
| * Set cme_err to 0 as this is not valid when result is not |
| * CME ERROR |
| */ |
| *cme_err = 0; |
| return true; |
| } |
| |
| if (strcmp(prefix, "ERROR") == 0) { |
| *result = HFP_RESULT_ERROR; |
| *cme_err = 0; |
| return true; |
| } |
| |
| if (strcmp(prefix, "NO CARRIER") == 0) { |
| *result = HFP_RESULT_NO_CARRIER; |
| *cme_err = 0; |
| return true; |
| } |
| |
| if (strcmp(prefix, "NO ANSWER") == 0) { |
| *result = HFP_RESULT_NO_ANSWER; |
| *cme_err = 0; |
| return true; |
| } |
| |
| if (strcmp(prefix, "BUSY") == 0) { |
| *result = HFP_RESULT_BUSY; |
| *cme_err = 0; |
| return true; |
| } |
| |
| if (strcmp(prefix, "DELAYED") == 0) { |
| *result = HFP_RESULT_DELAYED; |
| *cme_err = 0; |
| return true; |
| } |
| |
| if (strcmp(prefix, "BLACKLISTED") == 0) { |
| *result = HFP_RESULT_REJECTED; |
| *cme_err = 0; |
| return true; |
| } |
| |
| if (strcmp(prefix, "+CME ERROR") == 0) { |
| uint32_t val; |
| |
| *result = HFP_RESULT_CME_ERROR; |
| |
| if (hfp_context_get_number(context, &val) && |
| val <= HFP_ERROR_NETWORK_NOT_ALLOWED) |
| *cme_err = val; |
| else |
| *cme_err = HFP_ERROR_AG_FAILURE; |
| |
| return true; |
| } |
| |
| return false; |
| } |
| |
| static void hf_wakeup_writer(struct hfp_hf *hfp) |
| { |
| if (hfp->writer_active) |
| return; |
| |
| if (!ringbuf_len(hfp->write_buf)) |
| return; |
| |
| if (!io_set_write_handler(hfp->io, hf_can_write_data, |
| hfp, hf_write_watch_destroy)) |
| return; |
| |
| hfp->writer_active = true; |
| } |
| |
| static void hf_call_prefix_handler(struct hfp_hf *hfp, const char *data) |
| { |
| struct event_handler *handler; |
| const char *separators = ";:\0"; |
| struct hfp_context context; |
| enum hfp_result result; |
| enum hfp_error cme_err; |
| char lookup_prefix[18] = {}; |
| uint8_t pref_len = 0; |
| const char *prefix; |
| int i; |
| |
| context.offset = 0; |
| context.data = data; |
| |
| hf_skip_whitespace(&context); |
| |
| if (strlen(data + context.offset) < 2) |
| return; |
| |
| prefix = data + context.offset; |
| |
| pref_len = strcspn(prefix, separators); |
| if (pref_len > 17 || pref_len < 2) |
| return; |
| |
| for (i = 0; i < pref_len; i++) |
| lookup_prefix[i] = toupper(prefix[i]); |
| |
| lookup_prefix[pref_len] = '\0'; |
| context.offset += pref_len + 1; |
| |
| if (is_response(lookup_prefix, &result, &cme_err, &context)) { |
| struct cmd_response *cmd; |
| |
| cmd = queue_peek_head(hfp->cmd_queue); |
| if (!cmd) |
| return; |
| |
| cmd->resp_cb(result, cme_err, cmd->user_data); |
| |
| queue_remove(hfp->cmd_queue, cmd); |
| free(cmd); |
| |
| hf_wakeup_writer(hfp); |
| return; |
| } |
| |
| handler = queue_find(hfp->event_handlers, match_handler_event_prefix, |
| lookup_prefix); |
| if (!handler) |
| return; |
| |
| handler->callback(&context, handler->user_data); |
| } |
| |
| static char *find_cr_lf(char *str, size_t len) |
| { |
| char *ptr; |
| size_t count, offset; |
| |
| offset = 0; |
| |
| ptr = memchr(str, '\r', len); |
| while (ptr) { |
| /* |
| * Check if there is more data after '\r'. If so check for |
| * '\n' |
| */ |
| count = ptr - str; |
| if ((count < (len - 1)) && *(ptr + 1) == '\n') |
| return ptr; |
| |
| /* There is only '\r'? Let's try to find next one */ |
| offset += count + 1; |
| |
| if (offset >= len) |
| return NULL; |
| |
| ptr = memchr(str + offset, '\r', len - offset); |
| } |
| |
| return NULL; |
| } |
| |
| static void hf_process_input(struct hfp_hf *hfp) |
| { |
| char *str, *ptr, *str2, *tmp; |
| size_t len, count, offset, len2; |
| bool free_tmp = false; |
| |
| str = ringbuf_peek(hfp->read_buf, 0, &len); |
| if (!str) |
| return; |
| |
| offset = 0; |
| |
| ptr = find_cr_lf(str, len); |
| while (ptr) { |
| count = ptr - (str + offset); |
| if (count == 0) { |
| /* 2 is for <cr><lf> */ |
| offset += 2; |
| } else { |
| *ptr = '\0'; |
| hf_call_prefix_handler(hfp, str + offset); |
| offset += count + 2; |
| } |
| |
| ptr = find_cr_lf(str + offset, len - offset); |
| } |
| |
| /* |
| * Just check if there is no wrapped data in ring buffer. |
| * Should not happen too often |
| */ |
| if (len == ringbuf_len(hfp->read_buf)) |
| goto done; |
| |
| str2 = ringbuf_peek(hfp->read_buf, len, &len2); |
| if (!str2) |
| goto done; |
| |
| ptr = find_cr_lf(str2, len2); |
| if (!ptr) { |
| /* Might happen that we wrap between \r and \n */ |
| ptr = memchr(str2, '\n', len2); |
| if (!ptr) |
| goto done; |
| } |
| |
| count = ptr - str2; |
| |
| if (count) { |
| *ptr = '\0'; |
| |
| tmp = malloc(len + count); |
| if (!tmp) |
| goto done; |
| |
| /* "str" here is not a string so we need to use memcpy */ |
| memcpy(tmp, str, len); |
| memcpy(tmp + len, str2, count); |
| |
| free_tmp = true; |
| } else { |
| str[len-1] = '\0'; |
| tmp = str; |
| } |
| |
| hf_call_prefix_handler(hfp, tmp); |
| offset += count; |
| |
| done: |
| ringbuf_drain(hfp->read_buf, offset); |
| |
| if (free_tmp) |
| free(tmp); |
| } |
| |
| static bool hf_can_read_data(struct io *io, void *user_data) |
| { |
| struct hfp_hf *hfp = user_data; |
| ssize_t bytes_read; |
| |
| bytes_read = ringbuf_read(hfp->read_buf, hfp->fd); |
| if (bytes_read < 0) |
| return false; |
| |
| hf_process_input(hfp); |
| |
| return true; |
| } |
| |
| struct hfp_hf *hfp_hf_new(int fd) |
| { |
| struct hfp_hf *hfp; |
| |
| if (fd < 0) |
| return NULL; |
| |
| hfp = new0(struct hfp_hf, 1); |
| hfp->fd = fd; |
| hfp->close_on_unref = false; |
| |
| hfp->read_buf = ringbuf_new(4096); |
| if (!hfp->read_buf) { |
| free(hfp); |
| return NULL; |
| } |
| |
| hfp->write_buf = ringbuf_new(4096); |
| if (!hfp->write_buf) { |
| ringbuf_free(hfp->read_buf); |
| free(hfp); |
| return NULL; |
| } |
| |
| hfp->io = io_new(fd); |
| if (!hfp->io) { |
| ringbuf_free(hfp->write_buf); |
| ringbuf_free(hfp->read_buf); |
| free(hfp); |
| return NULL; |
| } |
| |
| hfp->event_handlers = queue_new(); |
| hfp->cmd_queue = queue_new(); |
| hfp->calls = queue_new(); |
| hfp->updated_calls = queue_new(); |
| hfp->writer_active = false; |
| |
| if (!io_set_read_handler(hfp->io, hf_can_read_data, hfp, |
| read_watch_destroy)) { |
| queue_destroy(hfp->event_handlers, |
| destroy_event_handler); |
| io_destroy(hfp->io); |
| ringbuf_free(hfp->write_buf); |
| ringbuf_free(hfp->read_buf); |
| free(hfp); |
| return NULL; |
| } |
| |
| return hfp_hf_ref(hfp); |
| } |
| |
| struct hfp_hf *hfp_hf_ref(struct hfp_hf *hfp) |
| { |
| if (!hfp) |
| return NULL; |
| |
| __sync_fetch_and_add(&hfp->ref_count, 1); |
| |
| return hfp; |
| } |
| |
| static void remove_call_cb(void *user_data) |
| { |
| struct hf_call *call = user_data; |
| struct hfp_hf *hfp = call->hfp; |
| |
| if (hfp->callbacks && hfp->callbacks->call_removed) |
| hfp->callbacks->call_removed(call->id, hfp->callbacks_data); |
| |
| free(call->line_id); |
| free(call); |
| } |
| |
| void hfp_hf_unref(struct hfp_hf *hfp) |
| { |
| if (!hfp) |
| return; |
| |
| if (__sync_sub_and_fetch(&hfp->ref_count, 1)) |
| return; |
| |
| io_set_write_handler(hfp->io, NULL, NULL, NULL); |
| io_set_read_handler(hfp->io, NULL, NULL, NULL); |
| io_set_disconnect_handler(hfp->io, NULL, NULL, NULL); |
| |
| io_destroy(hfp->io); |
| hfp->io = NULL; |
| |
| if (hfp->close_on_unref) |
| close(hfp->fd); |
| |
| hfp_hf_set_debug(hfp, NULL, NULL, NULL); |
| |
| ringbuf_free(hfp->read_buf); |
| hfp->read_buf = NULL; |
| |
| ringbuf_free(hfp->write_buf); |
| hfp->write_buf = NULL; |
| |
| queue_destroy(hfp->event_handlers, destroy_event_handler); |
| hfp->event_handlers = NULL; |
| |
| queue_destroy(hfp->cmd_queue, free); |
| hfp->cmd_queue = NULL; |
| |
| queue_destroy(hfp->calls, remove_call_cb); |
| hfp->calls = NULL; |
| |
| queue_destroy(hfp->updated_calls, NULL); |
| hfp->updated_calls = NULL; |
| |
| if (hfp->dialing_number) { |
| free(hfp->dialing_number); |
| hfp->dialing_number = NULL; |
| } |
| |
| if (!hfp->in_disconnect) { |
| free(hfp); |
| return; |
| } |
| |
| hfp->destroyed = true; |
| } |
| |
| static void hf_read_tracing(const void *buf, size_t count, |
| void *user_data) |
| { |
| struct hfp_hf *hfp = user_data; |
| |
| util_hexdump('>', buf, count, hfp->debug_callback, hfp->debug_data); |
| } |
| |
| static void hf_write_tracing(const void *buf, size_t count, |
| void *user_data) |
| { |
| struct hfp_hf *hfp = user_data; |
| |
| util_hexdump('<', buf, count, hfp->debug_callback, hfp->debug_data); |
| } |
| |
| bool hfp_hf_set_debug(struct hfp_hf *hfp, hfp_debug_func_t callback, |
| void *user_data, hfp_destroy_func_t destroy) |
| { |
| if (!hfp) |
| return false; |
| |
| if (hfp->debug_destroy) |
| hfp->debug_destroy(hfp->debug_data); |
| |
| hfp->debug_callback = callback; |
| hfp->debug_destroy = destroy; |
| hfp->debug_data = user_data; |
| |
| if (hfp->debug_callback) { |
| ringbuf_set_input_tracing(hfp->read_buf, hf_read_tracing, hfp); |
| ringbuf_set_input_tracing(hfp->write_buf, hf_write_tracing, |
| hfp); |
| } else { |
| ringbuf_set_input_tracing(hfp->read_buf, NULL, NULL); |
| ringbuf_set_input_tracing(hfp->write_buf, NULL, NULL); |
| } |
| |
| return true; |
| } |
| |
| bool hfp_hf_set_close_on_unref(struct hfp_hf *hfp, bool do_close) |
| { |
| if (!hfp) |
| return false; |
| |
| hfp->close_on_unref = do_close; |
| |
| return true; |
| } |
| |
| bool hfp_hf_send_command(struct hfp_hf *hfp, hfp_response_func_t resp_cb, |
| void *user_data, const char *format, ...) |
| { |
| va_list ap; |
| char *fmt; |
| int len; |
| struct cmd_response *cmd; |
| |
| if (!hfp || !format || !resp_cb) |
| return false; |
| |
| if (asprintf(&fmt, "%s\r", format) < 0) |
| return false; |
| |
| cmd = new0(struct cmd_response, 1); |
| |
| va_start(ap, format); |
| len = ringbuf_vprintf(hfp->write_buf, fmt, ap); |
| va_end(ap); |
| |
| free(fmt); |
| |
| if (len < 0) { |
| free(cmd); |
| return false; |
| } |
| |
| cmd->resp_cb = resp_cb; |
| cmd->user_data = user_data; |
| |
| if (!queue_push_tail(hfp->cmd_queue, cmd)) { |
| ringbuf_drain(hfp->write_buf, len); |
| free(cmd); |
| return false; |
| } |
| |
| hf_wakeup_writer(hfp); |
| |
| return true; |
| } |
| |
| bool hfp_hf_register(struct hfp_hf *hfp, hfp_hf_result_func_t callback, |
| const char *prefix, |
| void *user_data, |
| hfp_destroy_func_t destroy) |
| { |
| struct event_handler *handler; |
| |
| if (!callback) |
| return false; |
| |
| handler = new0(struct event_handler, 1); |
| handler->callback = callback; |
| handler->user_data = user_data; |
| |
| handler->prefix = strdup(prefix); |
| if (!handler->prefix) { |
| free(handler); |
| return false; |
| } |
| |
| if (queue_find(hfp->event_handlers, match_handler_event_prefix, |
| handler->prefix)) { |
| destroy_event_handler(handler); |
| return false; |
| } |
| |
| handler->destroy = destroy; |
| |
| return queue_push_tail(hfp->event_handlers, handler); |
| } |
| |
| bool hfp_hf_unregister(struct hfp_hf *hfp, const char *prefix) |
| { |
| struct cmd_handler *handler; |
| |
| /* Cast to void as queue_remove needs that */ |
| handler = queue_remove_if(hfp->event_handlers, |
| match_handler_event_prefix, |
| (void *) prefix); |
| |
| if (!handler) |
| return false; |
| |
| destroy_event_handler(handler); |
| |
| return true; |
| } |
| |
| static void hf_disconnect_watch_destroy(void *user_data) |
| { |
| struct hfp_hf *hfp = user_data; |
| |
| if (hfp->disconnect_destroy) |
| hfp->disconnect_destroy(hfp->disconnect_data); |
| |
| if (hfp->destroyed) |
| free(hfp); |
| } |
| |
| static bool hf_io_disconnected(struct io *io, void *user_data) |
| { |
| struct hfp_hf *hfp = user_data; |
| |
| hfp->in_disconnect = true; |
| |
| if (hfp->disconnect_callback) |
| hfp->disconnect_callback(hfp->disconnect_data); |
| |
| hfp->in_disconnect = false; |
| |
| return false; |
| } |
| |
| bool hfp_hf_set_disconnect_handler(struct hfp_hf *hfp, |
| hfp_disconnect_func_t callback, |
| void *user_data, |
| hfp_destroy_func_t destroy) |
| { |
| if (!hfp) |
| return false; |
| |
| if (hfp->disconnect_destroy) |
| hfp->disconnect_destroy(hfp->disconnect_data); |
| |
| if (!io_set_disconnect_handler(hfp->io, hf_io_disconnected, hfp, |
| hf_disconnect_watch_destroy)) { |
| hfp->disconnect_callback = NULL; |
| hfp->disconnect_destroy = NULL; |
| hfp->disconnect_data = NULL; |
| return false; |
| } |
| |
| hfp->disconnect_callback = callback; |
| hfp->disconnect_destroy = destroy; |
| hfp->disconnect_data = user_data; |
| |
| return true; |
| } |
| |
| bool hfp_hf_disconnect(struct hfp_hf *hfp) |
| { |
| if (!hfp) |
| return false; |
| |
| return io_shutdown(hfp->io); |
| } |
| |
| static bool call_id_match(const void *data, const void *match_data) |
| { |
| const struct hf_call *call = data; |
| uint id = PTR_TO_UINT(match_data); |
| |
| return (call->id == id); |
| } |
| |
| static uint next_call_index(struct hfp_hf *hfp) |
| { |
| for (uint i = 1; i < UINT_MAX; i++) { |
| if (!queue_find(hfp->calls, call_id_match, UINT_TO_PTR(i))) |
| return i; |
| } |
| |
| return 0; |
| } |
| |
| static struct hf_call *call_new(struct hfp_hf *hfp, unsigned int id, |
| enum hfp_call_status status, |
| char *number, unsigned int type, |
| bool mpty) |
| { |
| struct hf_call *call; |
| |
| call = new0(struct hf_call, 1); |
| call->id = id; |
| call->status = status; |
| if (number) |
| call->line_id = strdup(number); |
| call->type = type; |
| call->mpty = mpty; |
| call->hfp = hfp; |
| queue_push_tail(hfp->calls, call); |
| |
| if (hfp->callbacks && hfp->callbacks->call_added) |
| hfp->callbacks->call_added(call->id, call->status, |
| hfp->callbacks_data); |
| |
| return call; |
| } |
| |
| static void ciev_service_cb(uint8_t val, void *user_data) |
| { |
| struct hfp_hf *hfp = user_data; |
| |
| DBG(hfp, "%u", val); |
| |
| if (val < hfp->ag_ind[HFP_INDICATOR_SERVICE].min || |
| val > hfp->ag_ind[HFP_INDICATOR_SERVICE].max) { |
| DBG(hfp, "hf: Incorrect state: %u", val); |
| return; |
| } |
| |
| hfp->service = val; |
| if (hfp->callbacks && hfp->callbacks->update_indicator) |
| hfp->callbacks->update_indicator(HFP_INDICATOR_SERVICE, val, |
| hfp->callbacks_data); |
| } |
| |
| static void clcc_resp(enum hfp_result result, enum hfp_error cme_err, |
| void *user_data) |
| { |
| struct hfp_hf *hfp = user_data; |
| const struct queue_entry *call_entry, *id_entry; |
| struct hf_call *call; |
| uint id; |
| bool found; |
| struct queue *to_remove; |
| |
| DBG(hfp, ""); |
| |
| hfp->clcc_in_progress = false; |
| |
| if (result != HFP_RESULT_OK) { |
| DBG(hfp, "hf: CLCC error: %d", result); |
| goto failed; |
| } |
| |
| /* Removed disconnected calls */ |
| to_remove = queue_new(); |
| for (call_entry = queue_get_entries(hfp->calls); call_entry; |
| call_entry = call_entry->next) { |
| call = call_entry->data; |
| found = false; |
| |
| for (id_entry = queue_get_entries(hfp->updated_calls); |
| id_entry; id_entry = id_entry->next) { |
| id = PTR_TO_UINT(id_entry->data); |
| if (call->id == id) { |
| found = true; |
| break; |
| } |
| } |
| DBG(hfp, "hf: call %d -> %s", call->id, |
| found ? "updated" : "disconnected"); |
| |
| if (!found) |
| queue_push_tail(to_remove, UINT_TO_PTR(call->id)); |
| } |
| |
| for (id_entry = queue_get_entries(to_remove); |
| id_entry; id_entry = id_entry->next) { |
| id = PTR_TO_UINT(id_entry->data); |
| call = queue_find(hfp->calls, call_id_match, UINT_TO_PTR(id)); |
| if (!call) { |
| DBG(hfp, "hf: Unknown call to remove: %u", id); |
| continue; |
| } |
| queue_remove(hfp->calls, call); |
| remove_call_cb(call); |
| } |
| |
| queue_remove_all(hfp->updated_calls, NULL, NULL, NULL); |
| queue_destroy(to_remove, NULL); |
| return; |
| |
| failed: |
| if (!hfp->session && hfp->callbacks->session_ready) |
| hfp->callbacks->session_ready(result, cme_err, |
| hfp->callbacks_data); |
| } |
| |
| static bool send_clcc(struct hfp_hf *hfp) |
| { |
| if (!hfp->session || hfp->clcc_in_progress) |
| return true; |
| |
| if (!hfp_hf_send_command(hfp, clcc_resp, hfp, "AT+CLCC")) { |
| DBG(hfp, "hf: Could not send AT+CLCC"); |
| return false; |
| } |
| |
| hfp->clcc_in_progress = true; |
| |
| return true; |
| } |
| |
| static bool update_call_to_active(struct hfp_hf *hfp) |
| { |
| const struct queue_entry *entry; |
| struct hf_call *call; |
| |
| for (entry = queue_get_entries(hfp->calls); entry; |
| entry = entry->next) { |
| call = entry->data; |
| |
| if (call->status == CALL_STATUS_DIALING || |
| call->status == CALL_STATUS_ALERTING || |
| call->status == CALL_STATUS_INCOMING) { |
| call->status = CALL_STATUS_ACTIVE; |
| if (hfp->callbacks && |
| hfp->callbacks->call_status_updated) |
| hfp->callbacks->call_status_updated( |
| call->id, |
| call->status, |
| hfp->callbacks_data); |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| static void ciev_call_cb(uint8_t val, void *user_data) |
| { |
| struct hfp_hf *hfp = user_data; |
| uint id; |
| |
| DBG(hfp, "%u", val); |
| |
| if (hfp->features & HFP_AG_FEAT_ENHANCED_CALL_STATUS) { |
| send_clcc(hfp); |
| return; |
| } |
| |
| if (val < hfp->ag_ind[HFP_INDICATOR_CALL].min || |
| val > hfp->ag_ind[HFP_INDICATOR_CALL].max) { |
| DBG(hfp, "hf: Incorrect call state: %u", val); |
| return; |
| } |
| |
| switch (val) { |
| case CIND_CALL_NONE: |
| /* Remove all calls */ |
| queue_remove_all(hfp->calls, NULL, hfp, remove_call_cb); |
| break; |
| case CIND_CALL_IN_PROGRESS: |
| { |
| /* Find incoming, dialing or alerting call to change |
| * it to active |
| */ |
| if (update_call_to_active(hfp)) |
| return; |
| |
| /* else create new already active call */ |
| id = next_call_index(hfp); |
| if (id == 0) { |
| DBG(hfp, "hf: No new call index available"); |
| return; |
| } |
| call_new(hfp, id, CALL_STATUS_ACTIVE, NULL, 0, false); |
| } |
| break; |
| default: |
| DBG(hfp, "hf: Unsupported call state: %u", val); |
| } |
| } |
| |
| static bool call_outgoing_match(const void *data, const void *match_data) |
| { |
| const struct hf_call *call = data; |
| |
| return (call->status == CALL_STATUS_DIALING || |
| call->status == CALL_STATUS_ALERTING); |
| } |
| |
| static bool call_incoming_match(const void *data, const void *match_data) |
| { |
| const struct hf_call *call = data; |
| |
| return (call->status == CALL_STATUS_INCOMING); |
| } |
| |
| static bool call_setup_match(const void *data, const void *match_data) |
| { |
| return (call_outgoing_match(data, match_data) || |
| call_incoming_match(data, match_data)); |
| } |
| |
| static bool call_active_match(const void *data, const void *match_data) |
| { |
| const struct hf_call *call = data; |
| |
| return (call->status == CALL_STATUS_ACTIVE); |
| } |
| |
| static bool call_waiting_match(const void *data, const void *match_data) |
| { |
| const struct hf_call *call = data; |
| |
| return (call->status == CALL_STATUS_WAITING); |
| } |
| |
| static bool call_held_match(const void *data, const void *match_data) |
| { |
| const struct hf_call *call = data; |
| |
| return (call->status == CALL_STATUS_HELD); |
| } |
| |
| static void bsir_cb(struct hfp_context *context, void *user_data) |
| { |
| struct hfp_hf *hfp = user_data; |
| unsigned int val; |
| |
| DBG(hfp, ""); |
| |
| if (!hfp_context_get_number(context, &val)) |
| return; |
| |
| if (hfp->callbacks && hfp->callbacks->update_inband_ring) |
| hfp->callbacks->update_inband_ring(!!val, hfp->callbacks_data); |
| } |
| |
| static void ccwa_cb(struct hfp_context *context, void *user_data) |
| { |
| struct hfp_hf *hfp = user_data; |
| char number[255]; |
| unsigned int type; |
| struct hf_call *call; |
| uint id; |
| |
| DBG(hfp, ""); |
| |
| if (hfp->features & HFP_AG_FEAT_ENHANCED_CALL_STATUS) { |
| send_clcc(hfp); |
| return; |
| } |
| |
| if (!hfp_context_get_string(context, number, sizeof(number))) { |
| DBG(hfp, "hf: Could not get string"); |
| return; |
| } |
| |
| if (!hfp_context_get_number(context, &type)) |
| return; |
| |
| call = queue_find(hfp->calls, call_waiting_match, NULL); |
| if (call) { |
| DBG(hfp, "hf: waiting call already in progress"); |
| return; |
| } |
| |
| id = next_call_index(hfp); |
| if (id == 0) { |
| DBG(hfp, "hf: No new call index available"); |
| return; |
| } |
| call_new(hfp, id, CALL_STATUS_WAITING, number, type, false); |
| } |
| |
| static void ciev_callsetup_cb(uint8_t val, void *user_data) |
| { |
| struct hfp_hf *hfp = user_data; |
| struct hf_call *call; |
| uint id; |
| enum hfp_call_status status; |
| |
| DBG(hfp, "%u", val); |
| |
| if (hfp->features & HFP_AG_FEAT_ENHANCED_CALL_STATUS) { |
| send_clcc(hfp); |
| return; |
| } |
| |
| if (val < hfp->ag_ind[HFP_INDICATOR_CALLSETUP].min || |
| val > hfp->ag_ind[HFP_INDICATOR_CALLSETUP].max) { |
| DBG(hfp, "hf: Incorrect call setup state: %u", val); |
| return; |
| } |
| |
| switch (val) { |
| case CIND_CALLSETUP_NONE: |
| /* remove call in setup phase */ |
| queue_remove_all(hfp->calls, call_setup_match, hfp, |
| remove_call_cb); |
| break; |
| case CIND_CALLSETUP_INCOMING: |
| if (queue_length(hfp->calls) != 0) { |
| DBG(hfp, "hf: Call already exists"); |
| return; |
| } |
| |
| id = next_call_index(hfp); |
| if (id == 0) { |
| DBG(hfp, "hf: No new call index available"); |
| return; |
| } |
| call_new(hfp, id, CALL_STATUS_INCOMING, NULL, 0, false); |
| break; |
| case CIND_CALLSETUP_DIALING: |
| case CIND_CALLSETUP_ALERTING: |
| if (val == CIND_CALLSETUP_DIALING) |
| status = CALL_STATUS_DIALING; |
| else |
| status = CALL_STATUS_ALERTING; |
| |
| if (queue_find(hfp->calls, call_active_match, NULL)) { |
| DBG(hfp, "hf: Error: active call"); |
| return; |
| } |
| |
| call = queue_find(hfp->calls, call_outgoing_match, NULL); |
| if (call && call->status != status) { |
| call->status = status; |
| if (hfp->callbacks && |
| hfp->callbacks->call_status_updated) |
| hfp->callbacks->call_status_updated(call->id, |
| call->status, |
| hfp->callbacks_data); |
| return; |
| } |
| |
| id = next_call_index(hfp); |
| if (id == 0) { |
| DBG(hfp, "hf: No new call index available"); |
| return; |
| } |
| call_new(hfp, id, status, hfp->dialing_number, 0, false); |
| if (hfp->dialing_number) { |
| free(hfp->dialing_number); |
| hfp->dialing_number = NULL; |
| } |
| break; |
| } |
| } |
| |
| static void ciev_callheld_cb(uint8_t val, void *user_data) |
| { |
| struct hfp_hf *hfp = user_data; |
| |
| DBG(hfp, "%u", val); |
| |
| if (hfp->features & HFP_AG_FEAT_ENHANCED_CALL_STATUS) { |
| send_clcc(hfp); |
| return; |
| } |
| |
| if (val < hfp->ag_ind[HFP_INDICATOR_CALLHELD].min || |
| val > hfp->ag_ind[HFP_INDICATOR_CALLHELD].max) { |
| DBG(hfp, "hf: Incorrect call held state: %u", val); |
| return; |
| } |
| } |
| |
| static void ciev_signal_cb(uint8_t val, void *user_data) |
| { |
| struct hfp_hf *hfp = user_data; |
| |
| DBG(hfp, "%u", val); |
| |
| if (val < hfp->ag_ind[HFP_INDICATOR_SIGNAL].min || |
| val > hfp->ag_ind[HFP_INDICATOR_SIGNAL].max) { |
| DBG(hfp, "hf: Incorrect signal value: %u", val); |
| return; |
| } |
| |
| hfp->signal = val; |
| if (hfp->callbacks && hfp->callbacks->update_indicator) |
| hfp->callbacks->update_indicator(HFP_INDICATOR_SIGNAL, val, |
| hfp->callbacks_data); |
| } |
| |
| static void ciev_roam_cb(uint8_t val, void *user_data) |
| { |
| struct hfp_hf *hfp = user_data; |
| |
| DBG(hfp, "%u", val); |
| |
| if (val < hfp->ag_ind[HFP_INDICATOR_ROAM].min || |
| val > hfp->ag_ind[HFP_INDICATOR_ROAM].max) { |
| DBG(hfp, "hf: Incorrect roaming state: %u", val); |
| return; |
| } |
| |
| hfp->roaming = val; |
| if (hfp->callbacks && hfp->callbacks->update_indicator) |
| hfp->callbacks->update_indicator(HFP_INDICATOR_ROAM, val, |
| hfp->callbacks_data); |
| } |
| |
| static void ciev_battchg_cb(uint8_t val, void *user_data) |
| { |
| struct hfp_hf *hfp = user_data; |
| |
| DBG(hfp, "%u", val); |
| |
| if (val < hfp->ag_ind[HFP_INDICATOR_BATTCHG].min || |
| val > hfp->ag_ind[HFP_INDICATOR_BATTCHG].max) { |
| DBG(hfp, "hf: Incorrect battery charge value: %u", val); |
| return; |
| } |
| |
| hfp->battchg = val; |
| if (hfp->callbacks && hfp->callbacks->update_indicator) |
| hfp->callbacks->update_indicator(HFP_INDICATOR_BATTCHG, val, |
| hfp->callbacks_data); |
| } |
| |
| static void set_indicator_value(uint8_t index, unsigned int val, |
| struct indicator *ag_ind, struct hfp_hf *hfp) |
| { |
| int i; |
| |
| for (i = 0; i < HFP_INDICATOR_LAST; i++) { |
| if (index != ag_ind[i].index) |
| continue; |
| |
| ag_ind[i].val = val; |
| ag_ind[i].cb(val, hfp); |
| return; |
| } |
| } |
| |
| static void ciev_cb(struct hfp_context *context, void *user_data) |
| { |
| struct hfp_hf *hfp = user_data; |
| unsigned int index, val; |
| |
| DBG(hfp, ""); |
| |
| if (!hfp_context_get_number(context, &index)) |
| return; |
| |
| if (!hfp_context_get_number(context, &val)) |
| return; |
| |
| set_indicator_value(index, val, hfp->ag_ind, hfp); |
| } |
| |
| static void cops_cb(struct hfp_context *context, void *user_data) |
| { |
| struct hfp_hf *hfp = user_data; |
| unsigned int mode, val; |
| char name[255]; |
| |
| DBG(hfp, ""); |
| |
| if (!hfp_context_get_number(context, &mode)) |
| return; |
| |
| if (!hfp_context_get_number(context, &val)) |
| return; |
| |
| if (!hfp_context_get_string(context, name, sizeof(name))) { |
| DBG(hfp, "hf: Could not get string"); |
| return; |
| } |
| |
| if (hfp->callbacks && hfp->callbacks->update_operator) |
| hfp->callbacks->update_operator(name, hfp->callbacks_data); |
| } |
| |
| static void clcc_cb(struct hfp_context *context, void *user_data) |
| { |
| struct hfp_hf *hfp = user_data; |
| unsigned int id, status, mpty, type; |
| char number[255]; |
| struct hf_call *call; |
| |
| DBG(hfp, ""); |
| |
| if (!hfp_context_get_number(context, &id)) |
| return; |
| |
| /* Skip direction */ |
| hfp_context_skip_field(context); |
| |
| if (!hfp_context_get_number(context, &status)) |
| return; |
| |
| /* Skip mode */ |
| hfp_context_skip_field(context); |
| |
| if (!hfp_context_get_number(context, &mpty)) |
| return; |
| |
| if (!hfp_context_get_string(context, number, sizeof(number))) { |
| DBG(hfp, "hf: Could not get string"); |
| return; |
| } |
| |
| if (!hfp_context_get_number(context, &type)) |
| return; |
| |
| queue_push_tail(hfp->updated_calls, UINT_TO_PTR(id)); |
| |
| call = queue_find(hfp->calls, call_id_match, UINT_TO_PTR(id)); |
| if (!call) { |
| call_new(hfp, id, status, number, type, !!mpty); |
| return; |
| } |
| |
| if (call->status != status) { |
| call->status = status; |
| if (hfp->callbacks && hfp->callbacks->call_status_updated) |
| hfp->callbacks->call_status_updated(call->id, |
| call->status, hfp->callbacks_data); |
| } |
| |
| if (call->mpty != mpty) { |
| call->mpty = mpty; |
| if (hfp->callbacks && hfp->callbacks->call_mpty_updated) |
| hfp->callbacks->call_mpty_updated(call->id, |
| call->mpty, hfp->callbacks_data); |
| } |
| |
| if (call->line_id && strcmp(call->line_id, number) == 0 && |
| call->type == type) |
| return; |
| |
| if (call->line_id) |
| free(call->line_id); |
| call->line_id = strdup(number); |
| call->type = type; |
| |
| if (hfp->callbacks && hfp->callbacks->call_line_id_updated) |
| hfp->callbacks->call_line_id_updated(call->id, |
| call->line_id, |
| call->type, |
| hfp->callbacks_data); |
| } |
| |
| static void clip_cb(struct hfp_context *context, void *user_data) |
| { |
| struct hfp_hf *hfp = user_data; |
| char number[255]; |
| unsigned int type; |
| struct hf_call *call; |
| |
| DBG(hfp, ""); |
| |
| if (!hfp_context_get_string(context, number, sizeof(number))) { |
| DBG(hfp, "hf: Could not get string"); |
| return; |
| } |
| |
| if (!hfp_context_get_number(context, &type)) |
| return; |
| |
| call = queue_find(hfp->calls, call_incoming_match, NULL); |
| if (!call) { |
| DBG(hfp, "hf: no incoming call"); |
| return; |
| } |
| |
| if (call->line_id && strcmp(call->line_id, number) == 0 && |
| call->type == type) |
| return; |
| |
| if (call->line_id) |
| free(call->line_id); |
| call->line_id = strdup(number); |
| call->type = type; |
| |
| if (hfp->callbacks && hfp->callbacks->call_line_id_updated) |
| hfp->callbacks->call_line_id_updated(call->id, call->line_id, |
| call->type, |
| hfp->callbacks_data); |
| } |
| |
| static void nrec_resp(enum hfp_result result, enum hfp_error cme_err, |
| void *user_data) |
| { |
| struct hfp_hf *hfp = user_data; |
| |
| DBG(hfp, ""); |
| |
| if (result != HFP_RESULT_OK) { |
| DBG(hfp, "hf: NREC error: %d", result); |
| goto failed; |
| } |
| |
| hfp->session = true; |
| if (hfp->callbacks->session_ready) |
| hfp->callbacks->session_ready(HFP_RESULT_OK, 0, |
| hfp->callbacks_data); |
| |
| if (hfp->features & HFP_AG_FEAT_ENHANCED_CALL_STATUS) { |
| if (!send_clcc(hfp)) { |
| result = HFP_RESULT_ERROR; |
| goto failed; |
| } |
| } |
| |
| return; |
| |
| failed: |
| if (hfp->callbacks->session_ready) |
| hfp->callbacks->session_ready(result, cme_err, |
| hfp->callbacks_data); |
| } |
| |
| static void cmee_resp(enum hfp_result result, enum hfp_error cme_err, |
| void *user_data) |
| { |
| struct hfp_hf *hfp = user_data; |
| |
| DBG(hfp, ""); |
| |
| if (result != HFP_RESULT_OK) { |
| DBG(hfp, "hf: CMEE error: %d", result); |
| goto failed; |
| } |
| |
| if (!(hfp->features & HFP_AG_FEAT_ECNR)) { |
| /* Jump to next setup state */ |
| nrec_resp(HFP_RESULT_OK, cme_err, user_data); |
| return; |
| } |
| |
| if (!hfp_hf_send_command(hfp, nrec_resp, hfp, "AT+NREC=0")) { |
| DBG(hfp, "hf: Could not send AT+NREC=0"); |
| result = HFP_RESULT_ERROR; |
| goto failed; |
| } |
| |
| return; |
| |
| failed: |
| if (hfp->callbacks->session_ready) |
| hfp->callbacks->session_ready(result, cme_err, |
| hfp->callbacks_data); |
| } |
| |
| static void ccwa_resp(enum hfp_result result, enum hfp_error cme_err, |
| void *user_data) |
| { |
| struct hfp_hf *hfp = user_data; |
| |
| DBG(hfp, ""); |
| |
| if (result != HFP_RESULT_OK) { |
| DBG(hfp, "hf: CCWA error: %d", result); |
| goto failed; |
| } |
| |
| if (!(hfp->features & HFP_AG_FEAT_EXTENDED_RES_CODE)) { |
| /* Jump to next setup state */ |
| cmee_resp(HFP_RESULT_OK, cme_err, user_data); |
| return; |
| } |
| |
| if (!hfp_hf_send_command(hfp, cmee_resp, hfp, "AT+CMEE=1")) { |
| DBG(hfp, "hf: Could not send AT+CMEE=1"); |
| result = HFP_RESULT_ERROR; |
| goto failed; |
| } |
| |
| return; |
| |
| failed: |
| if (hfp->callbacks->session_ready) |
| hfp->callbacks->session_ready(result, cme_err, |
| hfp->callbacks_data); |
| } |
| |
| static void clip_resp(enum hfp_result result, enum hfp_error cme_err, |
| void *user_data) |
| { |
| struct hfp_hf *hfp = user_data; |
| |
| DBG(hfp, ""); |
| |
| if (result != HFP_RESULT_OK) { |
| DBG(hfp, "hf: CLIP error: %d", result); |
| goto failed; |
| } |
| |
| if (!(hfp->features & HFP_AG_FEAT_3WAY)) { |
| /* Jump to next setup state */ |
| ccwa_resp(HFP_RESULT_OK, cme_err, user_data); |
| return; |
| } |
| |
| if (!hfp_hf_send_command(hfp, ccwa_resp, hfp, |
| "AT+CCWA=1")) { |
| DBG(hfp, "hf: Could not send AT+CCWA=1"); |
| result = HFP_RESULT_ERROR; |
| goto failed; |
| } |
| |
| return; |
| |
| failed: |
| if (hfp->callbacks->session_ready) |
| hfp->callbacks->session_ready(result, cme_err, |
| hfp->callbacks_data); |
| } |
| |
| static void cops_resp(enum hfp_result result, enum hfp_error cme_err, |
| void *user_data) |
| { |
| struct hfp_hf *hfp = user_data; |
| |
| DBG(hfp, ""); |
| |
| if (result != HFP_RESULT_OK) { |
| DBG(hfp, "hf: COPS? error: %d", result); |
| goto failed; |
| } |
| |
| /* SLC creation done, continue with default setup */ |
| if (!hfp_hf_send_command(hfp, clip_resp, hfp, |
| "AT+CLIP=1")) { |
| DBG(hfp, "hf: Could not send AT+CLIP=1"); |
| result = HFP_RESULT_ERROR; |
| goto failed; |
| } |
| |
| return; |
| |
| failed: |
| if (hfp->callbacks->session_ready) |
| hfp->callbacks->session_ready(result, cme_err, |
| hfp->callbacks_data); |
| } |
| |
| static void cops_conf_resp(enum hfp_result result, enum hfp_error cme_err, |
| void *user_data) |
| { |
| struct hfp_hf *hfp = user_data; |
| |
| DBG(hfp, ""); |
| |
| if (result != HFP_RESULT_OK) { |
| DBG(hfp, "hf: COPS= error: %d", result); |
| goto failed; |
| } |
| |
| /* SLC creation done, continue with default setup */ |
| if (!hfp_hf_send_command(hfp, cops_resp, hfp, |
| "AT+COPS?")) { |
| DBG(hfp, "hf: Could not send AT+COPS?"); |
| result = HFP_RESULT_ERROR; |
| goto failed; |
| } |
| |
| return; |
| |
| failed: |
| if (hfp->callbacks->session_ready) |
| hfp->callbacks->session_ready(result, cme_err, |
| hfp->callbacks_data); |
| } |
| |
| static void slc_chld_cb(struct hfp_context *context, void *user_data) |
| { |
| struct hfp_hf *hfp = user_data; |
| |
| if (!hfp_context_open_container(context)) { |
| DBG(hfp, "hf: Could not open container for CHLD"); |
| return; |
| } |
| |
| while (hfp_context_has_next(context) && |
| !hfp_context_is_container_close(context)) { |
| char val[3]; |
| |
| if (!hfp_context_get_unquoted_string(context, val, |
| sizeof(val))) { |
| DBG(hfp, "hf: Could not get string"); |
| goto failed; |
| } |
| |
| if (strcmp(val, "0") == 0) |
| hfp->chlds |= HFP_CHLD_0; |
| else if (strcmp(val, "1") == 0) |
| hfp->chlds |= HFP_CHLD_1; |
| else if (strcmp(val, "2") == 0) |
| hfp->chlds |= HFP_CHLD_2; |
| else |
| DBG(hfp, "CHLD not supported: %s", val); |
| } |
| |
| if (!hfp_context_close_container(context)) { |
| DBG(hfp, "hf: Could not close container"); |
| goto failed; |
| } |
| |
| return; |
| failed: |
| DBG(hfp, "hf: Error on CHLD response"); |
| } |
| |
| static void slc_chld_resp(enum hfp_result result, enum hfp_error cme_err, |
| void *user_data) |
| { |
| struct hfp_hf *hfp = user_data; |
| |
| DBG(hfp, ""); |
| |
| hfp_hf_unregister(hfp, "+CHLD"); |
| |
| if (result != HFP_RESULT_OK) { |
| DBG(hfp, "hf: CHLD=? error: %d", result); |
| goto failed; |
| } |
| |
| /* SLC creation done, continue with default setup */ |
| if (!hfp_hf_send_command(hfp, cops_conf_resp, hfp, |
| "AT+COPS=3,0")) { |
| DBG(hfp, "hf: Could not send AT+COPS=3,0"); |
| result = HFP_RESULT_ERROR; |
| goto failed; |
| } |
| |
| /* Register unsolicited results handlers */ |
| if (hfp->features & HFP_AG_FEAT_IN_BAND_RING_TONE) |
| hfp_hf_register(hfp, bsir_cb, "+BSIR", hfp, NULL); |
| if (hfp->features & HFP_AG_FEAT_3WAY) |
| hfp_hf_register(hfp, ccwa_cb, "+CCWA", hfp, NULL); |
| hfp_hf_register(hfp, ciev_cb, "+CIEV", hfp, NULL); |
| hfp_hf_register(hfp, clcc_cb, "+CLCC", hfp, NULL); |
| hfp_hf_register(hfp, clip_cb, "+CLIP", hfp, NULL); |
| hfp_hf_register(hfp, cops_cb, "+COPS", hfp, NULL); |
| |
| return; |
| |
| failed: |
| if (hfp->callbacks->session_ready) |
| hfp->callbacks->session_ready(result, cme_err, |
| hfp->callbacks_data); |
| } |
| |
| static void slc_cmer_resp(enum hfp_result result, enum hfp_error cme_err, |
| void *user_data) |
| { |
| struct hfp_hf *hfp = user_data; |
| |
| DBG(hfp, ""); |
| |
| if (result != HFP_RESULT_OK) { |
| DBG(hfp, "hf: CMER error: %d", result); |
| goto failed; |
| } |
| |
| if (!(hfp->features & HFP_AG_FEAT_3WAY)) { |
| /* Jump to next setup state */ |
| slc_chld_resp(HFP_RESULT_OK, cme_err, user_data); |
| return; |
| } |
| |
| if (!hfp_hf_register(hfp, slc_chld_cb, "+CHLD", hfp, NULL)) { |
| DBG(hfp, "hf: Could not register +CHLD"); |
| result = HFP_RESULT_ERROR; |
| goto failed; |
| } |
| |
| if (!hfp_hf_send_command(hfp, slc_chld_resp, hfp, "AT+CHLD=?")) { |
| DBG(hfp, "hf: Could not send AT+CHLD=?"); |
| result = HFP_RESULT_ERROR; |
| goto failed; |
| } |
| |
| return; |
| |
| failed: |
| if (hfp->callbacks->session_ready) |
| hfp->callbacks->session_ready(result, cme_err, |
| hfp->callbacks_data); |
| } |
| |
| static void slc_cind_status_cb(struct hfp_context *context, |
| void *user_data) |
| { |
| struct hfp_hf *hfp = user_data; |
| uint8_t index = 1; |
| |
| while (hfp_context_has_next(context)) { |
| uint32_t val; |
| |
| if (!hfp_context_get_number(context, &val)) { |
| DBG(hfp, "hf: Error on CIND status response"); |
| return; |
| } |
| |
| set_indicator_value(index++, val, hfp->ag_ind, hfp); |
| } |
| } |
| |
| static void slc_cind_status_resp(enum hfp_result result, |
| enum hfp_error cme_err, |
| void *user_data) |
| { |
| struct hfp_hf *hfp = user_data; |
| |
| DBG(hfp, ""); |
| |
| hfp_hf_unregister(hfp, "+CIND"); |
| |
| if (result != HFP_RESULT_OK) { |
| DBG(hfp, "hf: CIND error: %d", result); |
| goto failed; |
| } |
| |
| /* Continue with SLC creation */ |
| if (!hfp_hf_send_command(hfp, slc_cmer_resp, hfp, |
| "AT+CMER=3,0,0,1")) { |
| DBG(hfp, "hf: Could not send AT+CMER"); |
| result = HFP_RESULT_ERROR; |
| goto failed; |
| } |
| |
| return; |
| |
| failed: |
| if (hfp->callbacks->session_ready) |
| hfp->callbacks->session_ready(result, cme_err, |
| hfp->callbacks_data); |
| } |
| |
| static void set_indicator_parameters(struct hfp_hf *hfp, uint8_t index, |
| const char *indicator, |
| unsigned int min, |
| unsigned int max) |
| { |
| struct indicator *ag_ind = hfp->ag_ind; |
| |
| DBG(hfp, "%s, %i", indicator, index); |
| |
| if (strcmp("service", indicator) == 0) { |
| if (min != 0 || max != 1) { |
| DBG(hfp, "hf: Invalid min/max values for service," |
| " expected (0,1) got (%u,%u)", min, max); |
| return; |
| } |
| ag_ind[HFP_INDICATOR_SERVICE].index = index; |
| ag_ind[HFP_INDICATOR_SERVICE].min = min; |
| ag_ind[HFP_INDICATOR_SERVICE].max = max; |
| ag_ind[HFP_INDICATOR_SERVICE].cb = ciev_service_cb; |
| return; |
| } |
| |
| if (strcmp("call", indicator) == 0) { |
| if (min != 0 || max != 1) { |
| DBG(hfp, "hf: Invalid min/max values for call," |
| " expected (0,1) got (%u,%u)", min, max); |
| return; |
| } |
| ag_ind[HFP_INDICATOR_CALL].index = index; |
| ag_ind[HFP_INDICATOR_CALL].min = min; |
| ag_ind[HFP_INDICATOR_CALL].max = max; |
| ag_ind[HFP_INDICATOR_CALL].cb = ciev_call_cb; |
| return; |
| } |
| |
| if (strcmp("callsetup", indicator) == 0) { |
| if (min != 0 || max != 3) { |
| DBG(hfp, "hf: Invalid min/max values for callsetup," |
| " expected (0,3) got (%u,%u)", min, max); |
| return; |
| } |
| ag_ind[HFP_INDICATOR_CALLSETUP].index = index; |
| ag_ind[HFP_INDICATOR_CALLSETUP].min = min; |
| ag_ind[HFP_INDICATOR_CALLSETUP].max = max; |
| ag_ind[HFP_INDICATOR_CALLSETUP].cb = ciev_callsetup_cb; |
| return; |
| } |
| |
| if (strcmp("callheld", indicator) == 0) { |
| if (min != 0 || max != 2) { |
| DBG(hfp, "hf: Invalid min/max values for callheld," |
| " expected (0,2) got (%u,%u)", min, max); |
| return; |
| } |
| ag_ind[HFP_INDICATOR_CALLHELD].index = index; |
| ag_ind[HFP_INDICATOR_CALLHELD].min = min; |
| ag_ind[HFP_INDICATOR_CALLHELD].max = max; |
| ag_ind[HFP_INDICATOR_CALLHELD].cb = ciev_callheld_cb; |
| return; |
| } |
| |
| if (strcmp("signal", indicator) == 0) { |
| if (min != 0 || max != 5) { |
| DBG(hfp, "hf: Invalid min/max values for signal," |
| " expected (0,5) got (%u,%u)", min, max); |
| return; |
| } |
| ag_ind[HFP_INDICATOR_SIGNAL].index = index; |
| ag_ind[HFP_INDICATOR_SIGNAL].min = min; |
| ag_ind[HFP_INDICATOR_SIGNAL].max = max; |
| ag_ind[HFP_INDICATOR_SIGNAL].cb = ciev_signal_cb; |
| return; |
| } |
| |
| if (strcmp("roam", indicator) == 0) { |
| if (min != 0 || max != 1) { |
| DBG(hfp, "hf: Invalid min/max values for roam," |
| " expected (0,1) got (%u,%u)", min, max); |
| return; |
| } |
| ag_ind[HFP_INDICATOR_ROAM].index = index; |
| ag_ind[HFP_INDICATOR_ROAM].min = min; |
| ag_ind[HFP_INDICATOR_ROAM].max = max; |
| ag_ind[HFP_INDICATOR_ROAM].cb = ciev_roam_cb; |
| return; |
| } |
| |
| if (strcmp("battchg", indicator) == 0) { |
| if (min != 0 || max != 5) { |
| DBG(hfp, "hf: Invalid min/max values for battchg," |
| " expected (0,5) got (%u,%u)", min, max); |
| return; |
| } |
| ag_ind[HFP_INDICATOR_BATTCHG].index = index; |
| ag_ind[HFP_INDICATOR_BATTCHG].min = min; |
| ag_ind[HFP_INDICATOR_BATTCHG].max = max; |
| ag_ind[HFP_INDICATOR_BATTCHG].cb = ciev_battchg_cb; |
| return; |
| } |
| |
| DBG(hfp, "hf: Unknown indicator: %s", indicator); |
| } |
| |
| static void slc_cind_cb(struct hfp_context *context, void *user_data) |
| { |
| struct hfp_hf *hfp = user_data; |
| int index = 1; |
| |
| DBG(hfp, ""); |
| |
| while (hfp_context_has_next(context)) { |
| char name[255]; |
| unsigned int min, max; |
| |
| /* e.g ("callsetup",(0-3)) */ |
| if (!hfp_context_open_container(context)) |
| break; |
| |
| if (!hfp_context_get_string(context, name, sizeof(name))) { |
| DBG(hfp, "hf: Could not get string"); |
| goto failed; |
| } |
| |
| if (!hfp_context_open_container(context)) { |
| DBG(hfp, "hf: Could not open container"); |
| goto failed; |
| } |
| |
| if (!hfp_context_get_range(context, &min, &max)) { |
| if (!hfp_context_get_number(context, &min)) { |
| DBG(hfp, "hf: Could not get number"); |
| goto failed; |
| } |
| |
| if (!hfp_context_get_number(context, &max)) { |
| DBG(hfp, "hf: Could not get number"); |
| goto failed; |
| } |
| } |
| |
| if (!hfp_context_close_container(context)) { |
| DBG(hfp, "hf: Could not close container"); |
| goto failed; |
| } |
| |
| if (!hfp_context_close_container(context)) { |
| DBG(hfp, "hf: Could not close container"); |
| goto failed; |
| } |
| |
| set_indicator_parameters(hfp, index, name, min, max); |
| index++; |
| } |
| |
| return; |
| |
| failed: |
| DBG(hfp, "hf: Error on CIND response"); |
| } |
| |
| static void slc_cind_resp(enum hfp_result result, enum hfp_error cme_err, |
| void *user_data) |
| { |
| struct hfp_hf *hfp = user_data; |
| |
| DBG(hfp, ""); |
| |
| hfp_hf_unregister(hfp, "+CIND"); |
| |
| if (result != HFP_RESULT_OK) { |
| DBG(hfp, "hf: CIND error: %d", result); |
| goto failed; |
| } |
| |
| /* Continue with SLC creation */ |
| if (!hfp_hf_register(hfp, slc_cind_status_cb, "+CIND", hfp, |
| NULL)) { |
| DBG(hfp, "hf: Could not register +CIND"); |
| result = HFP_RESULT_ERROR; |
| goto failed; |
| } |
| |
| if (!hfp_hf_send_command(hfp, slc_cind_status_resp, hfp, |
| "AT+CIND?")) { |
| DBG(hfp, "hf: Could not send AT+CIND?"); |
| result = HFP_RESULT_ERROR; |
| goto failed; |
| } |
| |
| return; |
| |
| failed: |
| if (hfp->callbacks->session_ready) |
| hfp->callbacks->session_ready(result, cme_err, |
| hfp->callbacks_data); |
| } |
| |
| static void slc_brsf_cb(struct hfp_context *context, void *user_data) |
| { |
| struct hfp_hf *hfp = user_data; |
| unsigned int feat; |
| |
| DBG(hfp, ""); |
| |
| if (hfp_context_get_number(context, &feat)) |
| hfp->features = feat; |
| } |
| |
| static void slc_brsf_resp(enum hfp_result result, enum hfp_error cme_err, |
| void *user_data) |
| { |
| struct hfp_hf *hfp = user_data; |
| |
| DBG(hfp, ""); |
| |
| hfp_hf_unregister(hfp, "+BRSF"); |
| |
| if (result != HFP_RESULT_OK) { |
| DBG(hfp, "BRSF error: %d", result); |
| goto failed; |
| } |
| |
| /* Continue with SLC creation */ |
| if (!hfp_hf_register(hfp, slc_cind_cb, "+CIND", hfp, NULL)) { |
| DBG(hfp, "hf: Could not register for +CIND"); |
| result = HFP_RESULT_ERROR; |
| goto failed; |
| } |
| |
| if (!hfp_hf_send_command(hfp, slc_cind_resp, hfp, "AT+CIND=?")) { |
| DBG(hfp, "hf: Could not send AT+CIND command"); |
| result = HFP_RESULT_ERROR; |
| goto failed; |
| } |
| |
| return; |
| |
| failed: |
| if (hfp->callbacks->session_ready) |
| hfp->callbacks->session_ready(result, cme_err, |
| hfp->callbacks_data); |
| } |
| |
| bool hfp_hf_session_register(struct hfp_hf *hfp, |
| struct hfp_hf_callbacks *callbacks, |
| void *callbacks_data) |
| { |
| if (!hfp) |
| return false; |
| |
| hfp->callbacks = callbacks; |
| hfp->callbacks_data = callbacks_data; |
| |
| return true; |
| } |
| |
| bool hfp_hf_session(struct hfp_hf *hfp) |
| { |
| if (!hfp) |
| return false; |
| |
| DBG(hfp, ""); |
| |
| if (!hfp_hf_register(hfp, slc_brsf_cb, "+BRSF", hfp, NULL)) |
| return false; |
| |
| return hfp_hf_send_command(hfp, slc_brsf_resp, hfp, |
| "AT+BRSF=%u", HFP_HF_FEATURES); |
| } |
| |
| const char *hfp_hf_call_get_number(struct hfp_hf *hfp, uint id) |
| { |
| struct hf_call *call; |
| |
| if (!hfp) |
| return NULL; |
| |
| DBG(hfp, ""); |
| |
| call = queue_find(hfp->calls, call_id_match, UINT_TO_PTR(id)); |
| if (!call) { |
| DBG(hfp, "hf: no call with id: %u", id); |
| return NULL; |
| } |
| |
| return call->line_id; |
| } |
| |
| bool hfp_hf_call_get_multiparty(struct hfp_hf *hfp, uint id, bool *mpty) |
| { |
| struct hf_call *call; |
| |
| if (!hfp) |
| return false; |
| |
| DBG(hfp, ""); |
| |
| call = queue_find(hfp->calls, call_id_match, UINT_TO_PTR(id)); |
| if (!call) { |
| DBG(hfp, "hf: no call with id: %u", id); |
| return false; |
| } |
| |
| *mpty = call->mpty; |
| |
| return true; |
| } |
| |
| bool hfp_hf_dial(struct hfp_hf *hfp, const char *number, |
| hfp_response_func_t resp_cb, |
| void *user_data) |
| { |
| const char *c; |
| int count = 0; |
| |
| if (!hfp) |
| return false; |
| |
| DBG(hfp, ""); |
| |
| if (number == NULL || strlen(number) == 0) |
| return hfp_hf_send_command(hfp, resp_cb, user_data, |
| "AT+BLDN"); |
| |
| if (number[0] == '>') { |
| for (c = number + 1; *c != '\0'; c++) { |
| if (!(*c >= '0' && *c <= '9')) |
| return false; |
| count++; |
| } |
| if (count < 1 || count > 10) |
| return false; |
| } else { |
| for (c = number; *c != '\0'; c++) { |
| if (!(*c >= '0' && *c <= '9') && |
| !(*c >= 'A' && *c <= 'D') && |
| *c != '#' && *c != '*' && |
| *c != '+' && *c != ',') |
| return false; |
| count++; |
| } |
| if (count < 1 || count > 80) |
| return false; |
| } |
| |
| if (hfp->dialing_number) |
| free(hfp->dialing_number); |
| hfp->dialing_number = strdup(number); |
| |
| return hfp_hf_send_command(hfp, resp_cb, user_data, "ATD%s;", number); |
| } |
| |
| bool hfp_hf_release_and_accept(struct hfp_hf *hfp, |
| hfp_response_func_t resp_cb, |
| void *user_data) |
| { |
| if (!hfp) |
| return false; |
| |
| DBG(hfp, ""); |
| |
| if (!(hfp->chlds & HFP_CHLD_1) || |
| (!queue_find(hfp->calls, call_waiting_match, NULL) && |
| !queue_find(hfp->calls, call_held_match, NULL))) |
| return false; |
| |
| return hfp_hf_send_command(hfp, resp_cb, user_data, "AT+CHLD=1"); |
| } |
| |
| bool hfp_hf_swap_calls(struct hfp_hf *hfp, |
| hfp_response_func_t resp_cb, |
| void *user_data) |
| { |
| if (!hfp) |
| return false; |
| |
| DBG(hfp, ""); |
| |
| if (!(hfp->chlds & HFP_CHLD_2)) |
| return false; |
| |
| return hfp_hf_send_command(hfp, resp_cb, user_data, "AT+CHLD=2"); |
| } |
| |
| bool hfp_hf_call_answer(struct hfp_hf *hfp, uint id, |
| hfp_response_func_t resp_cb, |
| void *user_data) |
| { |
| struct hf_call *call; |
| |
| if (!hfp) |
| return false; |
| |
| DBG(hfp, ""); |
| |
| call = queue_find(hfp->calls, call_id_match, UINT_TO_PTR(id)); |
| if (!call) { |
| DBG(hfp, "hf: no call with id: %u", id); |
| return false; |
| } |
| |
| if (call->status != CALL_STATUS_INCOMING) { |
| DBG(hfp, "hf: %d not in incoming call state: %u", |
| id, call->status); |
| return false; |
| } |
| |
| return hfp_hf_send_command(hfp, resp_cb, user_data, "ATA"); |
| } |
| |
| bool hfp_hf_call_hangup(struct hfp_hf *hfp, uint id, |
| hfp_response_func_t resp_cb, |
| void *user_data) |
| { |
| struct hf_call *call; |
| |
| if (!hfp) |
| return false; |
| |
| DBG(hfp, ""); |
| |
| call = queue_find(hfp->calls, call_id_match, UINT_TO_PTR(id)); |
| if (!call) { |
| DBG(hfp, "hf: no call with id: %u", id); |
| return false; |
| } |
| |
| if (call_setup_match(call, NULL) || call_active_match(call, NULL)) { |
| return hfp_hf_send_command(hfp, resp_cb, user_data, |
| "AT+CHUP"); |
| } else if ((call_waiting_match(call, NULL) || |
| call_held_match(call, NULL)) && |
| (hfp->chlds & HFP_CHLD_0)) { |
| return hfp_hf_send_command(hfp, resp_cb, user_data, |
| "AT+CHLD=0"); |
| } |
| |
| return false; |
| } |