import from github
authorkyle <kyle@amelia.(none)>
Tue, 1 Dec 2009 08:02:29 +0000 (02:02 -0600)
committerkyle <kyle@amelia.(none)>
Tue, 1 Dec 2009 08:02:29 +0000 (02:02 -0600)
devious.c [new file with mode: 0644]
devious.h [new file with mode: 0644]

diff --git a/devious.c b/devious.c
new file mode 100644 (file)
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 <kyle@pbx.org>
+ *
+ * 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 <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <glib.h>
+#include <gtk/gtk.h>
+#include <hildon/hildon.h>
+#include <libgupnp/gupnp-control-point.h>
+#include <libgupnp-av/gupnp-av.h>
+
+#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 (file)
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 <kyle@pbx.org>
+ *
+ * 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 */