From 1e550818a9d06516a8d28822fa33cdb66a1b73d6 Mon Sep 17 00:00:00 2001 From: kyle Date: Tue, 1 Dec 2009 02:02:29 -0600 Subject: [PATCH] import from github --- devious.c | 962 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ devious.h | 105 +++++++ 2 files changed, 1067 insertions(+) create mode 100644 devious.c create mode 100644 devious.h diff --git a/devious.c b/devious.c new file mode 100644 index 0000000..a219344 --- /dev/null +++ b/devious.c @@ -0,0 +1,962 @@ +/* + * Devious: UPNP Control Point for Maemo 5 + * + * Copyright (C) 2009 Kyle Cronan + * + * Author: Kyle Cronan + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; version 3 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "devious.h" + + +void add_content(GUPnPDIDLLiteParser *didl_parser, xmlNode *object_node, + gpointer user_data) +{ + struct browse_data *data = (struct browse_data *)user_data; + + char *title = gupnp_didl_lite_object_get_title(object_node); + char *id = gupnp_didl_lite_object_get_id(object_node); + gboolean container = gupnp_didl_lite_object_is_container(object_node); + GError *error = NULL; + GdkPixbuf *icon = gtk_icon_theme_load_icon(gtk_icon_theme_get_default(), + (container ? "general_folder" + : "general_audio_file"), + HILDON_ICON_PIXEL_SIZE_FINGER, + 0, &error); + GtkTreeIter iter; + gtk_list_store_append(data->list, &iter); + gtk_list_store_set(data->list, &iter, + COL_ICON, icon, COL_LABEL, title, + COL_ID, id, COL_CONTENT, data->content_dir, + COL_CONTAINER, container, -1); +} + +struct browse_data *browse_data_new(GUPnPServiceProxy *content_dir, + const char *id, guint32 starting_index, + GtkListStore *list) +{ + struct browse_data *data; + + data = g_slice_new(struct browse_data); + data->content_dir = g_object_ref(content_dir); + data->id = g_strdup(id); + data->starting_index = starting_index; + data->list = list; + + return data; +} + +void browse_data_free(struct browse_data *data) +{ + g_free(data->id); + g_object_unref(data->content_dir); + g_slice_free(struct browse_data, data); +} + +void browse_cb(GUPnPServiceProxy *content_dir, + GUPnPServiceProxyAction *action, gpointer user_data) +{ + struct browse_data *data; + char *didl_xml; + guint32 number_returned; + guint32 total_matches; + GError *error; + + data = (struct browse_data *)user_data; + didl_xml = NULL; + error = NULL; + + gupnp_service_proxy_end_action(content_dir, action, &error, + "Result", G_TYPE_STRING, &didl_xml, + "NumberReturned", G_TYPE_UINT, + &number_returned, + "TotalMatches", G_TYPE_UINT, &total_matches, + NULL); + if (didl_xml) { + guint32 remaining; + GError *error = NULL; + + if (!gupnp_didl_lite_parser_parse_didl(data->didl_parser, didl_xml, + add_content, data, &error)) { + g_warning("%s\n", error->message); + g_error_free(error); + } + g_free(didl_xml); + + data->starting_index += number_returned; + + /* See if we have more objects to get */ + remaining = total_matches - data->starting_index; + /* Keep browsing till we get each and every object */ + if (remaining != 0) browse(content_dir, data->id, data->starting_index, + MIN(remaining, MAX_BROWSE), + data->list, data->didl_parser); + } else if (error) { + GUPnPServiceInfo *info; + + info = GUPNP_SERVICE_INFO(content_dir); + g_warning("Failed to browse '%s': %s\n", + gupnp_service_info_get_location(info), + error->message); + + g_error_free(error); + } + + browse_data_free(data); +} + +void browse(GUPnPServiceProxy *content_dir, const char *container_id, + guint32 starting_index, guint32 requested_count, + GtkListStore *list, GUPnPDIDLLiteParser *didl_parser) +{ + struct browse_data *data; + data = browse_data_new(content_dir, container_id, starting_index, list); + + gupnp_service_proxy_begin_action(content_dir, "Browse", browse_cb, data, + "ObjectID", G_TYPE_STRING, container_id, + "BrowseFlag", G_TYPE_STRING, + "BrowseDirectChildren", + "Filter", G_TYPE_STRING, "*", + "StartingIndex", G_TYPE_UINT, + starting_index, + "RequestedCount", G_TYPE_UINT, + requested_count, + "SortCriteria", G_TYPE_STRING, "", + NULL); +} + +void update_container(GUPnPServiceProxy *content_dir, + const char *container_id) +{ +// TODO +// GtkTreeModel *model; +// GtkTreeIter container_iter; +} + +void on_container_update_ids(GUPnPServiceProxy *content_dir, + const char *variable, GValue *value, + gpointer user_data) +{ + char **tokens = g_strsplit(g_value_get_string(value), ",", 0); + guint i; + for (i=0; tokens[i] != NULL && tokens[i+1] != NULL; i+=2) { + update_container(content_dir, tokens[i]); + } + g_strfreev(tokens); +} + +void set_panarea_padding(GtkWidget *child, gpointer data) +{ + void set_child_padding(GtkWidget *child, gpointer user_data) + { + GtkBox *box = GTK_BOX(user_data); + gboolean expand, fill; + guint pad; + GtkPackType pack; + + gtk_box_query_child_packing(box, child, &expand, &fill, &pad, &pack); + gtk_box_set_child_packing(box, child, expand, fill, 0, pack); + } + + if (GTK_IS_CONTAINER(child)) + gtk_container_forall(GTK_CONTAINER(child), set_child_padding, child); +} + +GtkWidget *new_selector(GtkListStore *list) +{ + GtkWidget *selector = hildon_touch_selector_new(); + + HildonTouchSelectorColumn *column = + hildon_touch_selector_append_column(HILDON_TOUCH_SELECTOR(selector), + GTK_TREE_MODEL(list), NULL, NULL); + g_object_unref(list); + hildon_touch_selector_column_set_text_column(column, 1); + + hildon_touch_selector_set_hildon_ui_mode(HILDON_TOUCH_SELECTOR(selector), + HILDON_UI_MODE_NORMAL); + GtkCellRenderer *renderer; + renderer = gtk_cell_renderer_pixbuf_new(); + g_object_set(renderer, "xalign", ICON_XALIGN, NULL); + gtk_cell_renderer_set_fixed_size(renderer, ICON_WIDTH, ROW_HEIGHT); + gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(column), renderer, FALSE); + gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(column), renderer, + "pixbuf", 0, NULL); + + renderer = gtk_cell_renderer_text_new(); + g_object_set(renderer, "xalign", 0.0, NULL); + gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(column), renderer, TRUE); + gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(column), renderer, + "text", 1, NULL); + + gtk_container_forall(GTK_CONTAINER(selector), set_panarea_padding, NULL); + + gtk_widget_show_all(selector); + return selector; +} + +void transport_uri(GUPnPServiceProxy *av_transport, + GUPnPServiceProxyAction *action, gpointer user_data) +{ + GError *error = NULL; + if (gupnp_service_proxy_end_action(av_transport, action, &error, NULL)) { + /* TODO: do something with duration? */ + } else { + g_warning("Failed to set URI"); + g_error_free(error); + } +} + +gboolean mime_type_is_a(const char *mime_type1, const char *mime_type2) +{ + gboolean ret; + + char *content_type1 = g_content_type_from_mime_type(mime_type1); + char *content_type2 = g_content_type_from_mime_type(mime_type2); + if (content_type1 == NULL || content_type2 == NULL) { + /* Uknown content type, just do a simple comarison */ + ret = g_ascii_strcasecmp(mime_type1, mime_type2) == 0; + } else { + ret = g_content_type_is_a(content_type1, content_type2); + } + + g_free(content_type1); + g_free(content_type2); + + return ret; +} + +gboolean is_transport_compat(const gchar *renderer_protocol, + const gchar *renderer_host, + const gchar *item_protocol, + const gchar *item_host) +{ + if (g_ascii_strcasecmp(renderer_protocol, item_protocol) != 0 && + g_ascii_strcasecmp(renderer_protocol, "*") != 0) { + return FALSE; + } else if (g_ascii_strcasecmp("INTERNAL", renderer_protocol) == 0 && + g_ascii_strcasecmp(renderer_host, item_host) != 0) { + /* Host must be the same in case of INTERNAL protocol */ + return FALSE; + } else { + return TRUE; + } +} + +gboolean is_content_format_compat(const gchar *renderer_content_format, + const gchar *item_content_format) +{ + if(g_ascii_strcasecmp(renderer_content_format, "*") != 0 && + !mime_type_is_a(item_content_format, renderer_content_format)) { + return FALSE; + } else { + return TRUE; + } +} + +gchar *get_dlna_pn(gchar **additional_info_fields) +{ + gchar *pn = NULL; + gint i; + for (i = 0; additional_info_fields[i]; i++) { + pn = g_strstr_len(additional_info_fields[i], + strlen(additional_info_fields[i]), "DLNA.ORG_PN="); + if (pn) { + pn += 12; /* end of "DLNA.ORG_PN=" */ + break; + } + } + + return pn; +} + +gboolean is_additional_info_compat(const gchar *renderer_additional_info, + const gchar *item_additional_info) +{ + gboolean ret = FALSE; + + if (g_ascii_strcasecmp(renderer_additional_info, "*") == 0) { + return TRUE; + } + + char **renderer_tokens = g_strsplit(renderer_additional_info, ";", -1); + if (renderer_tokens == NULL) { + return FALSE; + } + + char **item_tokens = g_strsplit(item_additional_info, ";", -1); + if (item_tokens == NULL) { + goto no_item_tokens; + } + + char *renderer_pn = get_dlna_pn(renderer_tokens); + char *item_pn = get_dlna_pn(item_tokens); + if (renderer_pn == NULL || item_pn == NULL) { + goto no_renderer_pn; + } + + if (g_ascii_strcasecmp(renderer_pn, item_pn) == 0) { + ret = TRUE; + } + +no_renderer_pn: + g_strfreev(item_tokens); +no_item_tokens: + g_strfreev(renderer_tokens); + + return ret; +} + +gboolean is_protocol_info_compat(xmlNode *res_node, + const gchar *renderer_protocol) +{ + gchar *item_protocol; + gchar **item_proto_tokens; + gchar **renderer_proto_tokens; + gboolean ret = FALSE; + + item_protocol = gupnp_didl_lite_property_get_attribute(res_node, + "protocolInfo"); + if (!item_protocol) return FALSE; + + item_proto_tokens = g_strsplit(item_protocol, ":", 4); + renderer_proto_tokens = g_strsplit(renderer_protocol, ":", 4); + + if (!item_proto_tokens[0] || !item_proto_tokens[1] || + !item_proto_tokens[2] || !item_proto_tokens[3] || + !renderer_proto_tokens[0] || !renderer_proto_tokens[1] || + !renderer_proto_tokens[2] || !renderer_proto_tokens[3]) + goto return_point; + + if (is_transport_compat(renderer_proto_tokens[0], renderer_proto_tokens[2], + item_proto_tokens[0], item_proto_tokens[1]) && + is_content_format_compat(renderer_proto_tokens[2], + item_proto_tokens[2]) && + is_additional_info_compat(renderer_proto_tokens[3], + item_proto_tokens[3])) + ret = TRUE; + +return_point: + g_free(item_protocol); + g_strfreev(renderer_proto_tokens); + g_strfreev(item_proto_tokens); + + return ret; +} + +char *find_compat_uri_from_metadata(const char *metadata, char **duration, + struct proxy *renderer) +{ + char *uri = NULL; + void on_didl_item_available(GUPnPDIDLLiteParser *didl_parser, + xmlNode *item_node, gpointer user_data) + { + GList *resources = + gupnp_didl_lite_object_get_property(item_node, "res"); + if (!resources) return; + + int i; + for (i=0; renderer->protocols[i] && uri == NULL; i++) { + GList *res, *compat_res = NULL; + xmlNode *res_node; + for (res = resources; res != NULL; res = res->next) { + res_node = (xmlNode *)res->data; + + int j; + for (j=0; renderer->protocols[j]; j++) { + if (is_protocol_info_compat(res_node, + renderer->protocols[j])) { + compat_res = res; + break; + } + } + } + if (!compat_res) continue; + + res_node = (xmlNode *)compat_res->data; + uri = gupnp_didl_lite_property_get_value(res_node); + *duration = gupnp_didl_lite_property_get_attribute(res_node, + "duration"); + } + g_list_free(resources); + } + + GError *error = NULL; + /* Assumption: metadata only contains a single didl object */ + gupnp_didl_lite_parser_parse_didl(renderer->set->didl_parser, metadata, + on_didl_item_available, NULL, &error); + if (error) { + g_warning("%s\n", error->message); + g_error_free(error); + } + + return uri; +} + +struct proxy *current_renderer(struct proxy_set *proxy_set) +{ + int i = hildon_picker_button_get_active(proxy_set->renderer_picker); + GtkTreeIter iter; + gtk_tree_model_iter_nth_child(GTK_TREE_MODEL(proxy_set->renderer_list), + &iter, NULL, i); + char *udn; + gtk_tree_model_get(GTK_TREE_MODEL(proxy_set->renderer_list), &iter, + 1, &udn, -1); + + return g_hash_table_lookup(proxy_set->renderers, udn); +} + +void set_av_transport_uri(GUPnPServiceProxy *content_dir, + GUPnPServiceProxyAction *action, gpointer user_data) +{ + GError *error = NULL; + char *metadata; + gupnp_service_proxy_end_action(content_dir, action, &error, + "Result", G_TYPE_STRING, &metadata, NULL); + if (!metadata) return; + if (error) { + g_warning("Failed to get metadata for content"); + g_error_free(error); + } + + struct proxy_set *proxy_set = (struct proxy_set *)user_data; + struct proxy *renderer = current_renderer(proxy_set); + + char *duration; + char *uri = find_compat_uri_from_metadata(metadata, &duration, renderer); + if (!uri) { + g_warning("no compatible URI found."); + return; + } + + GUPnPServiceProxy *av_transport = GUPNP_SERVICE_PROXY( + gupnp_device_info_get_service(GUPNP_DEVICE_INFO(renderer->proxy), + AV_TRANSPORT)); + + gupnp_service_proxy_begin_action(av_transport, "SetAVTransportURI", + transport_uri, NULL, + "InstanceID", G_TYPE_UINT, 0, + "CurrentURI", G_TYPE_STRING, uri, + "CurrentURIMetaData", G_TYPE_STRING, + metadata, + NULL); +} + +void av_transport_action_cb(GUPnPServiceProxy *av_transport, + GUPnPServiceProxyAction *action, gpointer data) +{ + const char *action_name = (const char *)data; + GError *error = NULL; + + if (!gupnp_service_proxy_end_action(av_transport, action, &error, NULL)) { + g_warning("Failed to send action '%s': %s", + action_name, error->message); + g_error_free(error); + } +} + +void g_value_free(gpointer data) +{ + g_value_unset((GValue *)data); + g_slice_free(GValue, data); +} + +GHashTable *create_av_transport_args_hash(char **additional_args) +{ + GHashTable *args = g_hash_table_new_full(g_str_hash, g_str_equal, + NULL, g_value_free); + + GValue *instance_id = g_slice_alloc0(sizeof(GValue)); + g_value_init(instance_id, G_TYPE_UINT); + g_value_set_uint(instance_id, 0); + + g_hash_table_insert(args, "InstanceID", instance_id); + + if (additional_args) { + int i; + for (i=0; additional_args[i]; i += 2) { + GValue *value = g_slice_alloc0(sizeof(GValue)); + g_value_init(value, G_TYPE_STRING); + g_value_set_string(value, additional_args[i + 1]); + g_hash_table_insert(args, additional_args[i], value); + } + } + return args; +} + +void av_transport_send_action(GUPnPServiceProxy *av_transport, char *action, + char *additional_args[]) +{ + GHashTable *args = create_av_transport_args_hash(additional_args); + + gupnp_service_proxy_begin_action_hash(av_transport, action, + av_transport_action_cb, + (char *)action, + args); + g_hash_table_unref(args); +} + +void transport_selection(struct proxy *server, GtkTreeRowReference *row) +{ + server->current_selection = row; + + GtkTreeModel *model = gtk_tree_row_reference_get_model(row); + GtkTreePath *path = gtk_tree_row_reference_get_path(row); + + GtkTreeIter iter; + gtk_tree_model_get_iter(model, &iter, path); + + char *label; + char *id; + GUPnPServiceProxy *content; + gtk_tree_model_get(model, &iter, COL_LABEL, &label, + COL_CONTENT, &content, COL_ID, &id, -1); + + gupnp_service_proxy_begin_action(content, "Browse", + set_av_transport_uri, server->set, + "ObjectID", G_TYPE_STRING, id, + "BrowseFlag", G_TYPE_STRING, + "BrowseMetadata", + "Filter", G_TYPE_STRING, "*", + "StartingIndex", G_TYPE_UINT, 0, + "RequestedCount", G_TYPE_UINT, 0, + "SortCriteria", G_TYPE_STRING, "", + NULL); +} + +void play_button(GtkWidget *button, gpointer data) +{ + struct proxy_set *proxy_set = (struct proxy_set *)data; + struct proxy *renderer = current_renderer(proxy_set); + GUPnPServiceProxy *av_transport = GUPNP_SERVICE_PROXY( + gupnp_device_info_get_service(GUPNP_DEVICE_INFO(renderer->proxy), + AV_TRANSPORT)); + char *args[] = {"Speed", "1", NULL}; + av_transport_send_action(av_transport, "Play", args); +} + +GtkWidget *play_window(struct proxy *server, GtkTreeRowReference *row) +{ + GtkWidget *window = hildon_stackable_window_new(); + gtk_window_set_title(GTK_WINDOW(window), PLAY_WINDOW_TITLE); + + GtkWidget *vbox = gtk_vbox_new(FALSE, 0); + gtk_container_add(GTK_CONTAINER(window), vbox); + + GtkWidget *hbox = gtk_hbox_new(FALSE, 0); + gtk_box_pack_start(GTK_BOX(vbox), hbox, TRUE, TRUE, 6); + + GtkWidget *image = gtk_image_new(); + gtk_box_pack_start(GTK_BOX(hbox), image, FALSE, FALSE, 6); + + GtkWidget *inner_box = gtk_vbox_new(FALSE, 0); + gtk_box_pack_start(GTK_BOX(hbox), inner_box, TRUE, TRUE, 6); + + GtkWidget *button_box = gtk_hbutton_box_new(); + gtk_box_pack_start(GTK_BOX(vbox), button_box, FALSE, FALSE, 6); + + GtkWidget *button = + hildon_button_new_with_text(HILDON_SIZE_FINGER_HEIGHT, + HILDON_BUTTON_ARRANGEMENT_VERTICAL, + "Play", ""); + + g_signal_connect(button, "clicked", G_CALLBACK(play_button), server->set); + + gtk_box_pack_start(GTK_BOX(button_box), button, FALSE, FALSE, 0); + gtk_widget_show_all(GTK_WIDGET(vbox)); + + return window; +} + +void content_select(HildonTouchSelector *selector, gint column, gpointer data) +{ + GtkTreePath *path; + path = hildon_touch_selector_get_last_activated_row(selector, column); + if (!path) return; + + GtkTreeModel *model = hildon_touch_selector_get_model(selector, column); + GtkTreeIter iter; + gtk_tree_model_get_iter(model, &iter, path); + + GUPnPServiceProxy *content; + char *id, *label; + gboolean container; + gtk_tree_model_get(model, &iter, COL_CONTENT, &content, COL_ID, &id, + COL_CONTAINER, &container, + COL_LABEL, &label, -1); + + struct proxy *server = (struct proxy *)data; + GtkWidget *window; + + if (container) { + GtkListStore *view_list; + window = content_window(server, label, &view_list); + browse(content, id, 0, MAX_BROWSE, view_list, server->set->didl_parser); + gupnp_service_proxy_add_notify(content, "ContainerUpdateIDs", + G_TYPE_STRING, on_container_update_ids, + NULL); + gupnp_service_proxy_set_subscribed(content, TRUE); + } else { + GtkTreeRowReference *row = gtk_tree_row_reference_new(model, path); + transport_selection(server, row); + window = play_window(server, row); + } + + HildonWindowStack *stack = hildon_window_stack_get_default(); + hildon_window_stack_push_1(stack, HILDON_STACKABLE_WINDOW(window)); +} + +GtkWidget *content_window(struct proxy *server, char *title, + GtkListStore **view_list) +{ + GtkWidget *window = hildon_stackable_window_new(); + gtk_window_set_title(GTK_WINDOW(window), title); + + GtkListStore *list = gtk_list_store_new(NUM_COLS, GDK_TYPE_PIXBUF, + G_TYPE_STRING, G_TYPE_STRING, + G_TYPE_POINTER, G_TYPE_BOOLEAN); + + GtkWidget *selector = new_selector(list); + + g_signal_connect(G_OBJECT(selector), "changed", + G_CALLBACK(content_select), server); + + gtk_container_add(GTK_CONTAINER(window), selector); + + *view_list = list; + return window; +} + +void server_select(HildonTouchSelector *selector, gint column, gpointer data) +{ + GtkTreePath *path; + path = hildon_touch_selector_get_last_activated_row(selector, column); + if (!path) return; + + GtkTreeModel *model = hildon_touch_selector_get_model(selector, column); + GtkTreeIter iter; + gtk_tree_model_get_iter(model, &iter, path); + + char *udn; + gtk_tree_model_get(model, &iter, COL_ID, &udn, -1); + + GHashTable *servers = (GHashTable *)data; + struct proxy *server = g_hash_table_lookup(servers, udn); + + GUPnPServiceProxy *content_dir = GUPNP_SERVICE_PROXY( + gupnp_device_info_get_service(GUPNP_DEVICE_INFO(server->proxy), + CONTENT_DIR)); + + GtkListStore *view_list; + GtkWidget *window = content_window(server, server->name, &view_list); + + browse(content_dir, "0", 0, MAX_BROWSE, view_list, + server->set->didl_parser); + + gupnp_service_proxy_add_notify(content_dir, "ContainerUpdateIDs", + G_TYPE_STRING, on_container_update_ids, + NULL); + gupnp_service_proxy_set_subscribed(content_dir, TRUE); + + /* TODO: GList *child_devices = gupnp_device_info_list_devices(GUPNP_DEVICE_INFO(server->proxy)); */ + + HildonWindowStack *stack = hildon_window_stack_get_default(); + hildon_window_stack_push_1(stack, HILDON_STACKABLE_WINDOW(window)); +} + +GtkWidget *target_selector(struct proxy_set *proxy_set) +{ + GtkWidget *selector; + selector = hildon_touch_selector_new(); + + HildonTouchSelectorColumn *column = + hildon_touch_selector_append_column(HILDON_TOUCH_SELECTOR(selector), + GTK_TREE_MODEL(proxy_set->renderer_list), NULL, NULL); + g_object_unref(proxy_set->renderer_list); + hildon_touch_selector_column_set_text_column(column, 0); + + GtkCellRenderer *renderer = gtk_cell_renderer_text_new(); + g_object_set(renderer, "xalign", 0.0, NULL); + gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(column), renderer, TRUE); + gtk_cell_renderer_set_fixed_size(renderer, -1, ROW_HEIGHT); + gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(column), renderer, + "text", 0, NULL); + + gtk_widget_show_all(selector); + return selector; +} + +GtkWidget *main_menu(struct proxy_set *proxy_set) +{ + GtkWidget *menu = hildon_app_menu_new(); + GtkWidget *button; + button = hildon_picker_button_new(HILDON_SIZE_AUTO, + HILDON_BUTTON_ARRANGEMENT_VERTICAL); + hildon_button_set_title(HILDON_BUTTON(button), CHOOSE_TARGET); + + GtkWidget *selector = target_selector(proxy_set); + hildon_picker_button_set_selector(HILDON_PICKER_BUTTON(button), + HILDON_TOUCH_SELECTOR(selector)); + proxy_set->renderer_picker = HILDON_PICKER_BUTTON(button); + + hildon_app_menu_append(HILDON_APP_MENU(menu), GTK_BUTTON(button)); + + gtk_widget_show_all(menu); + return menu; +} + +GtkWidget *main_window(HildonProgram *program, struct proxy_set *proxy_set) +{ + GtkWidget *window = hildon_stackable_window_new(); + hildon_program_add_window(program, HILDON_WINDOW(window)); + + gtk_window_set_title(GTK_WINDOW(window), APPLICATION_NAME); + + GError *error = NULL; + proxy_set->icon = gtk_icon_theme_load_icon(gtk_icon_theme_get_default(), + "control_bluetooth_lan", + HILDON_ICON_PIXEL_SIZE_FINGER, + 0, &error); + proxy_set->server_list = gtk_list_store_new(NUM_COLS, GDK_TYPE_PIXBUF, + G_TYPE_STRING, G_TYPE_STRING, + G_TYPE_POINTER, G_TYPE_BOOLEAN); + + GtkWidget *selector = new_selector(proxy_set->server_list); + + g_signal_connect(G_OBJECT(selector), "changed", + G_CALLBACK(server_select), proxy_set->servers); + + gtk_container_add(GTK_CONTAINER(window), selector); + + GtkWidget *menu = main_menu(proxy_set); + hildon_window_set_app_menu(HILDON_WINDOW(window), HILDON_APP_MENU(menu)); + + return window; +} + +void protocol_info(GUPnPServiceProxy *cm, GUPnPServiceProxyAction *action, + gpointer user_data) +{ + gchar *sink_protocols; + GError *error = NULL; + const gchar *udn = gupnp_service_info_get_udn(GUPNP_SERVICE_INFO(cm)); + + if (!gupnp_service_proxy_end_action(cm, action, &error, "Sink", + G_TYPE_STRING, &sink_protocols, + NULL)) { + g_warning("Failed to get sink protocol info from " + "media renderer '%s':%s\n", + udn, error->message); + g_error_free(error); + return; + } + + struct proxy *server = (struct proxy *)user_data; + server->protocols = g_strsplit(sink_protocols, ",", 0); +} + +void add_renderer(GUPnPDeviceProxy *proxy, struct proxy_set *proxy_set) +{ + const char *udn = gupnp_device_info_get_udn(GUPNP_DEVICE_INFO(proxy)); + if (!udn) return; + + struct proxy *server = g_hash_table_lookup(proxy_set->renderers, udn); + if (server) return; + + char *name = gupnp_device_info_get_friendly_name(GUPNP_DEVICE_INFO(proxy)); + if (!name) return; + + server = (struct proxy *)g_malloc(sizeof(struct proxy)); + server->proxy = proxy; + server->name = name; + server->set = proxy_set; + + GtkTreeIter iter; + gtk_list_store_append(proxy_set->renderer_list, &iter); + gtk_list_store_set(proxy_set->renderer_list, &iter, + 0, server->name, 1, udn, -1); + + /* TODO: default to saved value */ + hildon_picker_button_set_active(proxy_set->renderer_picker, 0); + + GtkTreeModel *model = GTK_TREE_MODEL(proxy_set->renderer_list); + GtkTreePath *path = gtk_tree_model_get_path(model, &iter); + server->row = gtk_tree_row_reference_new(model, path); + + g_hash_table_replace(proxy_set->renderers, (char *)udn, server); + + GUPnPServiceProxy *cm = GUPNP_SERVICE_PROXY( + gupnp_device_info_get_service(GUPNP_DEVICE_INFO(proxy), + CONNECTION_MANAGER)); + gupnp_service_proxy_begin_action(cm, "GetProtocolInfo", protocol_info, + server, NULL); +} + +void add_server(GUPnPDeviceProxy *proxy, struct proxy_set *proxy_set) +{ + const char *udn = gupnp_device_info_get_udn(GUPNP_DEVICE_INFO(proxy)); + if (!udn) return; + + struct proxy *server = g_hash_table_lookup(proxy_set->servers, udn); + if (server) return; + + char *name = gupnp_device_info_get_friendly_name(GUPNP_DEVICE_INFO(proxy)); + GUPnPServiceInfo *content_dir = + gupnp_device_info_get_service(GUPNP_DEVICE_INFO(proxy), CONTENT_DIR); + + if (!name || !content_dir) return; + + server = (struct proxy *)g_malloc(sizeof(struct proxy)); + server->proxy = proxy; + server->name = name; + server->set = proxy_set; + + GtkTreeIter iter; + gtk_list_store_append(proxy_set->server_list, &iter); + gtk_list_store_set(proxy_set->server_list, &iter, + COL_ICON, proxy_set->icon, COL_LABEL, server->name, COL_ID, udn, + COL_CONTENT, NULL, COL_CONTAINER, TRUE, -1); + + GtkTreeModel *model = GTK_TREE_MODEL(proxy_set->server_list); + GtkTreePath *path = gtk_tree_model_get_path(model, &iter); + server->row = gtk_tree_row_reference_new(model, path); + + g_hash_table_replace(proxy_set->servers, (char *)udn, server); +} + +void remove_renderer(GUPnPDeviceProxy *proxy, struct proxy_set *proxy_set) +{ + const char *udn = gupnp_device_info_get_udn(GUPNP_DEVICE_INFO(proxy)); + struct proxy *server = g_hash_table_lookup(proxy_set->renderers, udn); + if (!server) return; + + GtkTreeModel *model = GTK_TREE_MODEL(proxy_set->renderer_list); + GtkTreeIter iter; + gtk_tree_model_get_iter(model, &iter, + gtk_tree_row_reference_get_path(server->row)); + gtk_list_store_remove(proxy_set->renderer_list, &iter); + g_hash_table_remove(proxy_set->renderers, udn); + free(server); + + /* TODO: change current selection if necessary */ +} + +void remove_server(GUPnPDeviceProxy *proxy, struct proxy_set *proxy_set) +{ + const char *udn = gupnp_device_info_get_udn(GUPNP_DEVICE_INFO(proxy)); + struct proxy *server = g_hash_table_lookup(proxy_set->servers, udn); + if (!server) return; + + GtkTreeModel *model = GTK_TREE_MODEL(proxy_set->server_list); + GtkTreeIter iter; + gtk_tree_model_get_iter(model, &iter, + gtk_tree_row_reference_get_path(server->row)); + gtk_list_store_remove(proxy_set->server_list, &iter); + g_hash_table_remove(proxy_set->servers, udn); + free(server); + + /* TODO: bring user back to server menu if necessary */ +} + +void device_proxy_available(GUPnPControlPoint *cp, + GUPnPDeviceProxy *proxy, gpointer user_data) +{ + struct proxy_set *proxy_set = (struct proxy_set *)user_data; + const char *type; + type = gupnp_device_info_get_device_type(GUPNP_DEVICE_INFO(proxy)); + + if (g_pattern_match_simple(MEDIA_RENDERER, type)) { + add_renderer(proxy, proxy_set); + } else if (g_pattern_match_simple(MEDIA_SERVER, type)) { + add_server(proxy, proxy_set); + } +} + +void device_proxy_unavailable(GUPnPControlPoint *cp, + GUPnPDeviceProxy *proxy, gpointer user_data) +{ + struct proxy_set *proxy_set = (struct proxy_set *)user_data; + const char *type; + type = gupnp_device_info_get_device_type(GUPNP_DEVICE_INFO(proxy)); + + if (g_pattern_match_simple(MEDIA_RENDERER, type)) { + remove_renderer(proxy, proxy_set); + } else if (g_pattern_match_simple(MEDIA_SERVER, type)) { + remove_server(proxy, proxy_set); + } +} + +void init_upnp(struct proxy_set *proxy_set) +{ + GError *error = NULL; + + g_type_init(); + + GUPnPContext *context = gupnp_context_new(NULL, NULL, 0, &error); + if (error) { + g_printerr("Error creating the GUPnP context: %s\n", error->message); + g_error_free(error); + exit(1); + } + + GUPnPControlPoint *cp = gupnp_control_point_new(context, "ssdp:all"); + + g_signal_connect(cp, "device-proxy-available", + G_CALLBACK(device_proxy_available), proxy_set); + g_signal_connect(cp, "device-proxy-unavailable", + G_CALLBACK(device_proxy_unavailable), proxy_set); + + gssdp_resource_browser_set_active(GSSDP_RESOURCE_BROWSER(cp), TRUE); +} + +int main(int argc, char *argv[]) +{ + g_thread_init(NULL); + + hildon_gtk_init(&argc, &argv); + + HildonProgram *program = hildon_program_get_instance(); + g_set_application_name(APPLICATION_NAME); + + struct proxy_set proxy_set; + proxy_set.renderers = g_hash_table_new(g_str_hash, g_str_equal); + proxy_set.servers = g_hash_table_new(g_str_hash, g_str_equal); + + proxy_set.renderer_list = gtk_list_store_new(2, G_TYPE_STRING, + G_TYPE_STRING); + proxy_set.didl_parser = gupnp_didl_lite_parser_new(); + GtkWidget *window = main_window(program, &proxy_set); + g_signal_connect(G_OBJECT(window), "destroy", + G_CALLBACK(gtk_main_quit), NULL); + + init_upnp(&proxy_set); + + gtk_widget_show(window); + gtk_main(); + + return 0; +} diff --git a/devious.h b/devious.h new file mode 100644 index 0000000..a880add --- /dev/null +++ b/devious.h @@ -0,0 +1,105 @@ +/* + * Devious: UPNP Control Point for Maemo 5 + * + * Copyright (C) 2009 Kyle Cronan + * + * Author: Kyle Cronan + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; version 3 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef INCLUDED_DEVIOUS_H +#define INCLUDED_DEVIOUS_H 1 + +#define APPLICATION_NAME "Devious" +#define PLAY_WINDOW_TITLE "Now playing" +#define CHOOSE_TARGET "Choose target" +#define ROW_HEIGHT 75 +#define ICON_WIDTH 90 +#define ICON_XALIGN 0.6 +#define MEDIA_RENDERER "urn:schemas-upnp-org:device:MediaRenderer:*" +#define MEDIA_SERVER "urn:schemas-upnp-org:device:MediaServer:*" +#define CONTENT_DIR "urn:schemas-upnp-org:service:ContentDirectory" +#define CONNECTION_MANAGER "urn:schemas-upnp-org:service:ConnectionManager" +#define AV_TRANSPORT "urn:schemas-upnp-org:service:AVTransport" +#define MAX_BROWSE 64 + +struct proxy { + GtkTreeRowReference *row; + GUPnPDeviceProxy *proxy; + char *name; + struct proxy_set *set; + char **protocols; + GtkTreeRowReference *current_selection; +}; + +struct proxy_set { + GHashTable *renderers; + GHashTable *servers; + GtkListStore *renderer_list; + GtkListStore *server_list; + GdkPixbuf *icon; + HildonPickerButton *renderer_picker; + GUPnPDIDLLiteParser *didl_parser; +}; + +struct browse_data { + GUPnPServiceProxy *content_dir; + gchar *id; + guint32 starting_index; + GtkListStore *list; + GUPnPDIDLLiteParser *didl_parser; +}; + +enum { + COL_ICON = 0, + COL_LABEL, + COL_ID, + COL_CONTENT, + COL_CONTAINER, + NUM_COLS +}; + + +void browse(GUPnPServiceProxy *content_dir, const char *container_id, + guint32 starting_index, guint32 requested_count, + GtkListStore *list, GUPnPDIDLLiteParser *didl_parser); + +void set_panarea_padding(GtkWidget *child, gpointer data); + +GtkWidget *new_selector(GtkListStore *list); + +void content_select(HildonTouchSelector *selector, gint column, gpointer data); + +GtkWidget *content_window(struct proxy *server, char *title, + GtkListStore **list); + +void server_select(HildonTouchSelector *selector, gint column, gpointer data); + +GtkWidget *main_window(HildonProgram *program, struct proxy_set *proxy_set); + +void add_renderer(GUPnPDeviceProxy *proxy, struct proxy_set *proxy_set); + +void add_server(GUPnPDeviceProxy *proxy, struct proxy_set *proxy_set); + +void remove_server(GUPnPDeviceProxy *proxy, struct proxy_set *proxy_set); + +void device_proxy_available(GUPnPControlPoint *cp, + GUPnPDeviceProxy *proxy, gpointer user_data); + +void init_upnp(struct proxy_set *proxy_set); + +#endif /* INCLUDED_DEVIOUS_H */ -- 1.7.9.5