| /* Copyright 2016 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 <dbus/dbus.h> |
| #include <errno.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <syslog.h> |
| |
| #include "cras_bt_adapter.h" |
| #include "cras_bt_constants.h" |
| #include "cras_bt_player.h" |
| #include "cras_dbus_util.h" |
| #include "cras_utf8.h" |
| #include "utlist.h" |
| |
| static void cras_bt_on_player_registered(DBusPendingCall *pending_call, |
| void *data) |
| { |
| DBusMessage *reply; |
| |
| reply = dbus_pending_call_steal_reply(pending_call); |
| dbus_pending_call_unref(pending_call); |
| |
| if (dbus_message_get_type(reply) == DBUS_MESSAGE_TYPE_ERROR) { |
| syslog(LOG_ERR, "RegisterPlayer returned error: %s", |
| dbus_message_get_error_name(reply)); |
| dbus_message_unref(reply); |
| return; |
| } |
| |
| dbus_message_unref(reply); |
| } |
| |
| /* Note that player properties will be used mostly for AVRCP qualification and |
| * not for normal use cases. The corresponding media events won't be routed by |
| * CRAS until we have a plan to provide general system API to handle media |
| * control. |
| */ |
| static struct cras_bt_player player = { |
| .object_path = CRAS_DEFAULT_PLAYER, |
| .playback_status = NULL, |
| .identity = NULL, |
| .loop_status = "None", |
| .shuffle = 0, |
| .metadata = NULL, |
| .position = 0, |
| .can_go_next = 0, |
| .can_go_prev = 0, |
| .can_play = 0, |
| .can_pause = 0, |
| .can_control = 0, |
| .message_cb = NULL, |
| }; |
| |
| int cras_bt_register_player(DBusConnection *conn, |
| const struct cras_bt_adapter *adapter) |
| { |
| const char *adapter_path; |
| DBusMessage *method_call; |
| DBusMessageIter message_iter, dict; |
| DBusPendingCall *pending_call; |
| |
| adapter_path = cras_bt_adapter_object_path(adapter); |
| method_call = dbus_message_new_method_call(BLUEZ_SERVICE, adapter_path, |
| BLUEZ_INTERFACE_MEDIA, |
| "RegisterPlayer"); |
| if (!method_call) |
| return -ENOMEM; |
| |
| dbus_message_iter_init_append(method_call, &message_iter); |
| dbus_message_iter_append_basic(&message_iter, DBUS_TYPE_OBJECT_PATH, |
| &player.object_path); |
| |
| dbus_message_iter_open_container( |
| &message_iter, DBUS_TYPE_ARRAY, |
| DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING DBUS_TYPE_STRING_AS_STRING |
| DBUS_TYPE_VARIANT_AS_STRING |
| DBUS_DICT_ENTRY_END_CHAR_AS_STRING, |
| &dict); |
| |
| append_key_value(&dict, "PlaybackStatus", DBUS_TYPE_STRING, |
| DBUS_TYPE_STRING_AS_STRING, &player.playback_status); |
| append_key_value(&dict, "Identity", DBUS_TYPE_STRING, |
| DBUS_TYPE_STRING_AS_STRING, &player.identity); |
| append_key_value(&dict, "LoopStatus", DBUS_TYPE_STRING, |
| DBUS_TYPE_STRING_AS_STRING, &player.loop_status); |
| append_key_value(&dict, "Position", DBUS_TYPE_INT64, |
| DBUS_TYPE_INT64_AS_STRING, &player.position); |
| append_key_value(&dict, "Shuffle", DBUS_TYPE_BOOLEAN, |
| DBUS_TYPE_BOOLEAN_AS_STRING, &player.shuffle); |
| append_key_value(&dict, "CanGoNext", DBUS_TYPE_BOOLEAN, |
| DBUS_TYPE_BOOLEAN_AS_STRING, &player.can_go_next); |
| append_key_value(&dict, "CanGoPrevious", DBUS_TYPE_BOOLEAN, |
| DBUS_TYPE_BOOLEAN_AS_STRING, &player.can_go_prev); |
| append_key_value(&dict, "CanPlay", DBUS_TYPE_BOOLEAN, |
| DBUS_TYPE_BOOLEAN_AS_STRING, &player.can_play); |
| append_key_value(&dict, "CanPause", DBUS_TYPE_BOOLEAN, |
| DBUS_TYPE_BOOLEAN_AS_STRING, &player.can_pause); |
| append_key_value(&dict, "CanControl", DBUS_TYPE_BOOLEAN, |
| DBUS_TYPE_BOOLEAN_AS_STRING, &player.can_control); |
| |
| dbus_message_iter_close_container(&message_iter, &dict); |
| |
| if (!dbus_connection_send_with_reply(conn, method_call, &pending_call, |
| DBUS_TIMEOUT_USE_DEFAULT)) { |
| dbus_message_unref(method_call); |
| return -ENOMEM; |
| } |
| |
| dbus_message_unref(method_call); |
| if (!pending_call) |
| return -EIO; |
| |
| if (!dbus_pending_call_set_notify(pending_call, |
| cras_bt_on_player_registered, &player, |
| NULL)) { |
| dbus_pending_call_cancel(pending_call); |
| dbus_pending_call_unref(pending_call); |
| return -ENOMEM; |
| } |
| return 0; |
| } |
| |
| static void cras_bt_on_player_unregistered(DBusPendingCall *pending_call, |
| void *data) |
| { |
| DBusMessage *reply; |
| |
| reply = dbus_pending_call_steal_reply(pending_call); |
| dbus_pending_call_unref(pending_call); |
| |
| if (dbus_message_get_type(reply) == DBUS_MESSAGE_TYPE_ERROR) |
| syslog(LOG_WARNING, "UnregisterPlayer returned error: %s", |
| dbus_message_get_error_name(reply)); |
| |
| dbus_message_unref(reply); |
| } |
| int cras_bt_unregister_player(DBusConnection *conn, |
| const struct cras_bt_adapter *adapter) |
| { |
| const char *adapter_path; |
| DBusMessage *method_call; |
| DBusPendingCall *pending_call; |
| |
| adapter_path = cras_bt_adapter_object_path(adapter); |
| method_call = dbus_message_new_method_call(BLUEZ_SERVICE, adapter_path, |
| BLUEZ_INTERFACE_MEDIA, |
| "UnregisterPlayer"); |
| if (!method_call) |
| return -ENOMEM; |
| |
| if (!dbus_message_append_args(method_call, DBUS_TYPE_OBJECT_PATH, |
| &player.object_path, DBUS_TYPE_INVALID)) { |
| dbus_message_unref(method_call); |
| return -ENOMEM; |
| } |
| |
| if (!dbus_connection_send_with_reply(conn, method_call, &pending_call, |
| DBUS_TIMEOUT_USE_DEFAULT)) { |
| dbus_message_unref(method_call); |
| return -ENOMEM; |
| } |
| |
| dbus_message_unref(method_call); |
| if (!pending_call) |
| return -EIO; |
| |
| if (!dbus_pending_call_set_notify(pending_call, |
| cras_bt_on_player_unregistered, |
| &player, NULL)) { |
| dbus_pending_call_cancel(pending_call); |
| dbus_pending_call_unref(pending_call); |
| return -ENOMEM; |
| } |
| return 0; |
| } |
| |
| static DBusHandlerResult cras_bt_player_handle_message(DBusConnection *conn, |
| DBusMessage *message, |
| void *arg) |
| { |
| const char *msg = dbus_message_get_member(message); |
| |
| if (player.message_cb) |
| player.message_cb(msg); |
| |
| return DBUS_HANDLER_RESULT_HANDLED; |
| } |
| |
| static struct cras_bt_player_metadata *cras_bt_player_metadata_init() |
| { |
| struct cras_bt_player_metadata *metadata = |
| malloc(sizeof(struct cras_bt_player_metadata)); |
| metadata->title = calloc(1, CRAS_PLAYER_METADATA_SIZE_MAX); |
| metadata->album = calloc(1, CRAS_PLAYER_METADATA_SIZE_MAX); |
| metadata->artist = calloc(1, CRAS_PLAYER_METADATA_SIZE_MAX); |
| metadata->length = 0; |
| |
| return metadata; |
| } |
| |
| static void cras_bt_player_init() |
| { |
| player.playback_status = malloc(CRAS_PLAYER_PLAYBACK_STATUS_SIZE_MAX); |
| player.identity = malloc(CRAS_PLAYER_IDENTITY_SIZE_MAX); |
| |
| strcpy(player.playback_status, CRAS_PLAYER_PLAYBACK_STATUS_DEFAULT); |
| strcpy(player.identity, CRAS_PLAYER_IDENTITY_DEFAULT); |
| player.position = 0; |
| |
| player.metadata = cras_bt_player_metadata_init(); |
| } |
| |
| static void cras_bt_player_append_metadata_artist(DBusMessageIter *iter, |
| const char *artist) |
| { |
| DBusMessageIter dict, varient, array; |
| const char *artist_key = "xesam:artist"; |
| |
| dbus_message_iter_open_container(iter, DBUS_TYPE_DICT_ENTRY, NULL, |
| &dict); |
| dbus_message_iter_append_basic(&dict, DBUS_TYPE_STRING, &artist_key); |
| dbus_message_iter_open_container( |
| &dict, DBUS_TYPE_VARIANT, |
| DBUS_TYPE_ARRAY_AS_STRING DBUS_TYPE_STRING_AS_STRING, &varient); |
| dbus_message_iter_open_container(&varient, DBUS_TYPE_ARRAY, |
| DBUS_TYPE_STRING_AS_STRING, &array); |
| dbus_message_iter_append_basic(&array, DBUS_TYPE_STRING, &artist); |
| dbus_message_iter_close_container(&varient, &array); |
| dbus_message_iter_close_container(&dict, &varient); |
| dbus_message_iter_close_container(iter, &dict); |
| } |
| |
| static void cras_bt_player_append_metadata(DBusMessageIter *iter, |
| const char *title, |
| const char *artist, |
| const char *album, |
| dbus_int64_t length) |
| { |
| DBusMessageIter varient, array; |
| dbus_message_iter_open_container( |
| iter, DBUS_TYPE_VARIANT, |
| DBUS_TYPE_ARRAY_AS_STRING DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING |
| DBUS_TYPE_STRING_AS_STRING DBUS_TYPE_VARIANT_AS_STRING |
| DBUS_DICT_ENTRY_END_CHAR_AS_STRING, |
| &varient); |
| dbus_message_iter_open_container( |
| &varient, DBUS_TYPE_ARRAY, |
| DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING DBUS_TYPE_STRING_AS_STRING |
| DBUS_TYPE_VARIANT_AS_STRING |
| DBUS_DICT_ENTRY_END_CHAR_AS_STRING, |
| &array); |
| if (!is_utf8_string(title)) { |
| syslog(LOG_INFO, "Non-utf8 title: %s", title); |
| title = ""; |
| } |
| if (!is_utf8_string(album)) { |
| syslog(LOG_INFO, "Non-utf8 album: %s", album); |
| album = ""; |
| } |
| if (!is_utf8_string(artist)) { |
| syslog(LOG_INFO, "Non-utf8 artist: %s", artist); |
| artist = ""; |
| } |
| |
| append_key_value(&array, "xesam:title", DBUS_TYPE_STRING, |
| DBUS_TYPE_STRING_AS_STRING, &title); |
| append_key_value(&array, "xesam:album", DBUS_TYPE_STRING, |
| DBUS_TYPE_STRING_AS_STRING, &album); |
| append_key_value(&array, "mpris:length", DBUS_TYPE_INT64, |
| DBUS_TYPE_INT64_AS_STRING, &length); |
| cras_bt_player_append_metadata_artist(&array, artist); |
| |
| dbus_message_iter_close_container(&varient, &array); |
| dbus_message_iter_close_container(iter, &varient); |
| } |
| |
| static bool cras_bt_player_parse_metadata(const char *title, const char *album, |
| const char *artist, |
| const dbus_int64_t length) |
| { |
| bool require_update = false; |
| |
| if (title && strcmp(player.metadata->title, title)) { |
| snprintf(player.metadata->title, CRAS_PLAYER_METADATA_SIZE_MAX, |
| "%s", title); |
| require_update = true; |
| } |
| if (artist && strcmp(player.metadata->artist, artist)) { |
| snprintf(player.metadata->artist, CRAS_PLAYER_METADATA_SIZE_MAX, |
| "%s", artist); |
| require_update = true; |
| } |
| if (album && strcmp(player.metadata->album, album)) { |
| snprintf(player.metadata->album, CRAS_PLAYER_METADATA_SIZE_MAX, |
| "%s", album); |
| require_update = true; |
| } |
| if (length && player.metadata->length != length) { |
| player.metadata->length = length; |
| require_update = true; |
| } |
| |
| return require_update; |
| } |
| |
| int cras_bt_player_create(DBusConnection *conn) |
| { |
| static const DBusObjectPathVTable player_vtable = { |
| .message_function = cras_bt_player_handle_message |
| }; |
| |
| DBusError dbus_error; |
| struct cras_bt_adapter **adapters; |
| size_t num_adapters, i; |
| |
| dbus_error_init(&dbus_error); |
| |
| cras_bt_player_init(); |
| if (!dbus_connection_register_object_path( |
| conn, player.object_path, &player_vtable, &dbus_error)) { |
| syslog(LOG_ERR, "Cannot register player %s", |
| player.object_path); |
| dbus_error_free(&dbus_error); |
| return -ENOMEM; |
| } |
| |
| num_adapters = cras_bt_adapter_get_list(&adapters); |
| for (i = 0; i < num_adapters; ++i) |
| cras_bt_register_player(conn, adapters[i]); |
| free(adapters); |
| return 0; |
| } |
| |
| int cras_bt_player_destroy(DBusConnection *conn) |
| { |
| struct cras_bt_adapter **adapters; |
| size_t num_adapters, i; |
| |
| num_adapters = cras_bt_adapter_get_list(&adapters); |
| |
| for (i = 0; i < num_adapters; ++i) |
| cras_bt_unregister_player(conn, adapters[i]); |
| free(adapters); |
| |
| return dbus_connection_unregister_object_path(conn, player.object_path); |
| } |
| |
| int cras_bt_player_update_playback_status(DBusConnection *conn, |
| const char *status) |
| { |
| DBusMessage *msg; |
| DBusMessageIter iter, dict; |
| const char *playerInterface = BLUEZ_INTERFACE_MEDIA_PLAYER; |
| |
| if (!player.playback_status) |
| return -ENXIO; |
| |
| /* Verify the string value matches one of the possible status defined in |
| * bluez/profiles/audio/avrcp.c |
| */ |
| if (strcasecmp(status, "stopped") != 0 && |
| strcasecmp(status, "playing") != 0 && |
| strcasecmp(status, "paused") != 0 && |
| strcasecmp(status, "forward-seek") != 0 && |
| strcasecmp(status, "reverse-seek") != 0 && |
| strcasecmp(status, "error") != 0) |
| return -EINVAL; |
| |
| if (!strcasecmp(player.playback_status, status)) |
| return 0; |
| |
| strcpy(player.playback_status, status); |
| |
| msg = dbus_message_new_signal(CRAS_DEFAULT_PLAYER, |
| DBUS_INTERFACE_PROPERTIES, |
| "PropertiesChanged"); |
| if (!msg) |
| return -ENOMEM; |
| |
| dbus_message_iter_init_append(msg, &iter); |
| dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, |
| &playerInterface); |
| dbus_message_iter_open_container( |
| &iter, DBUS_TYPE_ARRAY, |
| DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING DBUS_TYPE_STRING_AS_STRING |
| DBUS_TYPE_VARIANT_AS_STRING |
| DBUS_DICT_ENTRY_END_CHAR_AS_STRING, |
| &dict); |
| append_key_value(&dict, "PlaybackStatus", DBUS_TYPE_STRING, |
| DBUS_TYPE_STRING_AS_STRING, &status); |
| dbus_message_iter_close_container(&iter, &dict); |
| |
| if (!dbus_connection_send(conn, msg, NULL)) { |
| dbus_message_unref(msg); |
| return -ENOMEM; |
| } |
| |
| dbus_message_unref(msg); |
| return 0; |
| } |
| |
| int cras_bt_player_update_identity(DBusConnection *conn, const char *identity) |
| { |
| DBusMessage *msg; |
| DBusMessageIter iter, dict; |
| const char *playerInterface = BLUEZ_INTERFACE_MEDIA_PLAYER; |
| |
| if (!player.identity) |
| return -ENXIO; |
| |
| if (!identity) |
| return -EINVAL; |
| |
| if (!is_utf8_string(identity)) { |
| syslog(LOG_INFO, "Non-utf8 identity: %s", identity); |
| identity = ""; |
| } |
| |
| if (!strcasecmp(player.identity, identity)) |
| return 0; |
| |
| strcpy(player.identity, identity); |
| |
| msg = dbus_message_new_signal(CRAS_DEFAULT_PLAYER, |
| DBUS_INTERFACE_PROPERTIES, |
| "PropertiesChanged"); |
| if (!msg) |
| return -ENOMEM; |
| |
| dbus_message_iter_init_append(msg, &iter); |
| dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, |
| &playerInterface); |
| dbus_message_iter_open_container( |
| &iter, DBUS_TYPE_ARRAY, |
| DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING DBUS_TYPE_STRING_AS_STRING |
| DBUS_TYPE_VARIANT_AS_STRING |
| DBUS_DICT_ENTRY_END_CHAR_AS_STRING, |
| &dict); |
| append_key_value(&dict, "Identity", DBUS_TYPE_STRING, |
| DBUS_TYPE_STRING_AS_STRING, &identity); |
| dbus_message_iter_close_container(&iter, &dict); |
| |
| if (!dbus_connection_send(conn, msg, NULL)) { |
| dbus_message_unref(msg); |
| return -ENOMEM; |
| } |
| |
| dbus_message_unref(msg); |
| return 0; |
| } |
| |
| int cras_bt_player_update_position(DBusConnection *conn, |
| const dbus_int64_t position) |
| { |
| DBusMessage *msg; |
| DBusMessageIter iter, dict; |
| const char *playerInterface = BLUEZ_INTERFACE_MEDIA_PLAYER; |
| |
| if (position < 0) |
| return -EINVAL; |
| |
| player.position = position; |
| |
| msg = dbus_message_new_signal(CRAS_DEFAULT_PLAYER, |
| DBUS_INTERFACE_PROPERTIES, |
| "PropertiesChanged"); |
| if (!msg) |
| return -ENOMEM; |
| |
| dbus_message_iter_init_append(msg, &iter); |
| dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, |
| &playerInterface); |
| dbus_message_iter_open_container( |
| &iter, DBUS_TYPE_ARRAY, |
| DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING DBUS_TYPE_STRING_AS_STRING |
| DBUS_TYPE_VARIANT_AS_STRING |
| DBUS_DICT_ENTRY_END_CHAR_AS_STRING, |
| &dict); |
| append_key_value(&dict, "Position", DBUS_TYPE_INT64, |
| DBUS_TYPE_INT64_AS_STRING, &player.position); |
| dbus_message_iter_close_container(&iter, &dict); |
| |
| if (!dbus_connection_send(conn, msg, NULL)) { |
| dbus_message_unref(msg); |
| return -ENOMEM; |
| } |
| |
| dbus_message_unref(msg); |
| return 0; |
| } |
| |
| int cras_bt_player_update_metadata(DBusConnection *conn, const char *title, |
| const char *artist, const char *album, |
| const dbus_int64_t length) |
| { |
| DBusMessage *msg; |
| DBusMessageIter iter, array, dict; |
| const char *property = "Metadata"; |
| const char *playerInterface = BLUEZ_INTERFACE_MEDIA_PLAYER; |
| |
| if (!player.metadata) |
| return -ENXIO; |
| |
| msg = dbus_message_new_signal(CRAS_DEFAULT_PLAYER, |
| DBUS_INTERFACE_PROPERTIES, |
| "PropertiesChanged"); |
| if (!msg) |
| return -ENOMEM; |
| |
| dbus_message_iter_init_append(msg, &iter); |
| dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, |
| &playerInterface); |
| dbus_message_iter_open_container( |
| &iter, DBUS_TYPE_ARRAY, |
| DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING DBUS_TYPE_STRING_AS_STRING |
| DBUS_TYPE_VARIANT_AS_STRING |
| DBUS_DICT_ENTRY_END_CHAR_AS_STRING, |
| &array); |
| dbus_message_iter_open_container(&array, DBUS_TYPE_DICT_ENTRY, NULL, |
| &dict); |
| dbus_message_iter_append_basic(&dict, DBUS_TYPE_STRING, &property); |
| |
| if (!cras_bt_player_parse_metadata(title, album, artist, length)) { |
| /* Nothing to update. */ |
| dbus_message_unref(msg); |
| return 0; |
| } |
| |
| cras_bt_player_append_metadata(&dict, player.metadata->title, |
| player.metadata->artist, |
| player.metadata->album, |
| player.metadata->length); |
| |
| dbus_message_iter_close_container(&array, &dict); |
| dbus_message_iter_close_container(&iter, &array); |
| |
| if (!dbus_connection_send(conn, msg, NULL)) { |
| dbus_message_unref(msg); |
| return -ENOMEM; |
| } |
| |
| dbus_message_unref(msg); |
| return 0; |
| } |