empty commit message
[devious] / devious.c
1 /*
2  * Devious: UPNP Control Point for Maemo 5
3  *
4  * Copyright (C) 2009 Kyle Cronan
5  *
6  * Author: Kyle Cronan <kyle@pbx.org>
7  *
8  * This program is free software; you can redistribute it and/or modify it
9  * under the terms of the GNU General Public License as published by the
10  * Free Software Foundation; version 3 of the License, or (at your option)
11  * any later version.
12  *
13  * This program is distributed in the hope that it will be useful, but
14  * WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
16  * Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License along
19  * with this library; if not, write to the Free Software Foundation, Inc.,
20  * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
21  *
22  */
23
24
25 #include <stdio.h>
26 #include <stdlib.h>
27 #include <string.h>
28 #include <glib.h>
29 #include <gtk/gtk.h>
30 #include <hildon/hildon.h>
31 #include <libgupnp/gupnp-control-point.h>
32 #include <libgupnp-av/gupnp-av.h>
33
34 #include "devious.h"
35
36
37 void add_content(GUPnPDIDLLiteParser *didl_parser, xmlNode *object_node,
38                  gpointer user_data)
39 {
40     struct browse_data *data = (struct browse_data *)user_data;
41     
42     char *title = gupnp_didl_lite_object_get_title(object_node);
43     char *id = gupnp_didl_lite_object_get_id(object_node);
44     gboolean container = gupnp_didl_lite_object_is_container(object_node);
45     GError *error = NULL;
46     GdkPixbuf *icon = gtk_icon_theme_load_icon(gtk_icon_theme_get_default(),
47                                                (container ? "general_folder"
48                                                    : "general_audio_file"),
49                                                HILDON_ICON_PIXEL_SIZE_FINGER,
50                                                0, &error);
51     GtkTreeIter iter;
52     gtk_list_store_append(data->list, &iter);
53     gtk_list_store_set(data->list, &iter,
54                        COL_ICON, icon, COL_LABEL, title,
55                        COL_ID, id, COL_CONTENT, data->content_dir,
56                        COL_CONTAINER, container, -1);
57 }
58
59 struct browse_data *browse_data_new(GUPnPServiceProxy *content_dir,
60                                     const char *id, guint32 starting_index,
61                                     GtkListStore *list)
62 {
63     struct browse_data *data;
64
65     data = g_slice_new(struct browse_data);
66     data->content_dir = g_object_ref(content_dir);
67     data->id = g_strdup(id);
68     data->starting_index = starting_index;
69     data->list = list;
70
71     return data;
72 }
73
74 void browse_data_free(struct browse_data *data)
75 {
76     g_free(data->id);
77     g_object_unref(data->content_dir);
78     g_slice_free(struct browse_data, data);
79 }
80
81 void browse_cb(GUPnPServiceProxy *content_dir,
82                GUPnPServiceProxyAction *action, gpointer user_data)
83 {
84     struct browse_data *data;
85     char *didl_xml;
86     guint32 number_returned;
87     guint32 total_matches;
88     GError *error;
89
90     data = (struct browse_data *)user_data;
91     didl_xml = NULL;
92     error = NULL;
93
94     gupnp_service_proxy_end_action(content_dir, action, &error,
95                                    "Result", G_TYPE_STRING, &didl_xml,
96                                    "NumberReturned", G_TYPE_UINT,
97                                        &number_returned,
98                                    "TotalMatches", G_TYPE_UINT, &total_matches,
99                                    NULL);
100     if (didl_xml) {
101         guint32 remaining;
102         GError *error = NULL;
103
104         if (!gupnp_didl_lite_parser_parse_didl(data->didl_parser, didl_xml,
105                                                add_content, data, &error)) {
106             g_warning("%s\n", error->message);
107             g_error_free(error);
108         }
109         g_free(didl_xml);
110
111         data->starting_index += number_returned;
112
113         /* See if we have more objects to get */
114         remaining = total_matches - data->starting_index;
115         /* Keep browsing till we get each and every object */
116         if (remaining != 0) browse(content_dir, data->id, data->starting_index,
117                                    MIN(remaining, MAX_BROWSE),
118                                    data->list, data->didl_parser);
119     } else if (error) {
120         GUPnPServiceInfo *info;
121
122         info = GUPNP_SERVICE_INFO(content_dir);
123         g_warning("Failed to browse '%s': %s\n",
124                   gupnp_service_info_get_location(info),
125                   error->message);
126
127         g_error_free(error);
128     }
129
130     browse_data_free(data);
131 }
132
133 void browse(GUPnPServiceProxy *content_dir, const char *container_id,
134             guint32 starting_index, guint32 requested_count,
135             GtkListStore *list, GUPnPDIDLLiteParser *didl_parser)
136 {
137     struct browse_data *data;
138     data = browse_data_new(content_dir, container_id, starting_index, list);
139
140     gupnp_service_proxy_begin_action(content_dir, "Browse", browse_cb, data,
141                                      "ObjectID", G_TYPE_STRING, container_id,
142                                      "BrowseFlag", G_TYPE_STRING, 
143                                          "BrowseDirectChildren",
144                                      "Filter", G_TYPE_STRING, "*",
145                                      "StartingIndex", G_TYPE_UINT,
146                                          starting_index,
147                                      "RequestedCount", G_TYPE_UINT,
148                                          requested_count,
149                                      "SortCriteria", G_TYPE_STRING, "",
150                                      NULL);
151 }
152
153 void update_container(GUPnPServiceProxy *content_dir,
154                       const char *container_id)
155 {
156 //    TODO
157 //    GtkTreeModel *model;
158 //    GtkTreeIter container_iter;
159 }
160
161 void on_container_update_ids(GUPnPServiceProxy *content_dir,
162                              const char *variable, GValue *value,
163                              gpointer user_data)
164 {
165     char **tokens = g_strsplit(g_value_get_string(value), ",", 0);
166     guint i;
167     for (i=0; tokens[i] != NULL && tokens[i+1] != NULL; i+=2) {
168         update_container(content_dir, tokens[i]);
169     }
170     g_strfreev(tokens);
171 }
172
173 void set_panarea_padding(GtkWidget *child, gpointer data)
174 {
175     void set_child_padding(GtkWidget *child, gpointer user_data)
176     {
177         GtkBox *box = GTK_BOX(user_data);
178         gboolean expand, fill;
179         guint pad;
180         GtkPackType pack;
181
182         gtk_box_query_child_packing(box, child, &expand, &fill, &pad, &pack);
183         gtk_box_set_child_packing(box, child, expand, fill, 0, pack);
184     }
185
186     if (GTK_IS_CONTAINER(child))
187         gtk_container_forall(GTK_CONTAINER(child), set_child_padding, child);
188 }
189
190 GtkWidget *new_selector(GtkListStore *list)
191 {
192     GtkWidget *selector = hildon_touch_selector_new();
193
194     HildonTouchSelectorColumn *column =
195         hildon_touch_selector_append_column(HILDON_TOUCH_SELECTOR(selector),
196                                             GTK_TREE_MODEL(list), NULL, NULL);
197     g_object_unref(list);
198     hildon_touch_selector_column_set_text_column(column, 1);
199
200     hildon_touch_selector_set_hildon_ui_mode(HILDON_TOUCH_SELECTOR(selector),
201                                              HILDON_UI_MODE_NORMAL);
202     GtkCellRenderer *renderer;
203     renderer = gtk_cell_renderer_pixbuf_new();
204     g_object_set(renderer, "xalign", ICON_XALIGN, NULL);
205     gtk_cell_renderer_set_fixed_size(renderer, ICON_WIDTH, ROW_HEIGHT);
206     gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(column), renderer, FALSE);
207     gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(column), renderer,
208                                    "pixbuf", 0, NULL);
209
210     renderer = gtk_cell_renderer_text_new();
211     g_object_set(renderer, "xalign", 0.0, NULL);
212     gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(column), renderer, TRUE);
213     gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(column), renderer,
214                                    "text", 1, NULL);
215
216     gtk_container_forall(GTK_CONTAINER(selector), set_panarea_padding, NULL);
217
218     gtk_widget_show_all(selector);
219     return selector;
220 }
221
222 void transport_uri(GUPnPServiceProxy *av_transport,
223                    GUPnPServiceProxyAction *action, gpointer user_data)
224 {
225     GError *error = NULL;
226     if (gupnp_service_proxy_end_action(av_transport, action, &error, NULL)) {
227         /* TODO: do something with duration? */
228     } else {
229         g_warning("Failed to set URI");
230         g_error_free(error);
231     }
232 }
233
234 gboolean mime_type_is_a(const char *mime_type1, const char *mime_type2)
235 {
236     gboolean ret;
237
238     char *content_type1 = g_content_type_from_mime_type(mime_type1);
239     char *content_type2 = g_content_type_from_mime_type(mime_type2);
240     if (content_type1 == NULL || content_type2 == NULL) {
241         /* Uknown content type, just do a simple comarison */
242         ret = g_ascii_strcasecmp(mime_type1, mime_type2) == 0;
243     } else {
244         ret = g_content_type_is_a(content_type1, content_type2);
245     }
246
247     g_free(content_type1);
248     g_free(content_type2);
249
250     return ret;
251 }
252
253 gboolean is_transport_compat(const gchar *renderer_protocol,
254                              const gchar *renderer_host,
255                              const gchar *item_protocol,
256                              const gchar *item_host)
257 {
258     if (g_ascii_strcasecmp(renderer_protocol, item_protocol) != 0 &&
259         g_ascii_strcasecmp(renderer_protocol, "*") != 0) {
260         return FALSE;
261     } else if (g_ascii_strcasecmp("INTERNAL", renderer_protocol) == 0 &&
262                g_ascii_strcasecmp(renderer_host, item_host) != 0) {
263                /* Host must be the same in case of INTERNAL protocol */
264         return FALSE;
265     } else {
266         return TRUE;
267     }
268 }
269
270 gboolean is_content_format_compat(const gchar *renderer_content_format,
271                                   const gchar *item_content_format)
272 {
273     if(g_ascii_strcasecmp(renderer_content_format, "*") != 0 &&
274         !mime_type_is_a(item_content_format, renderer_content_format)) {
275         return FALSE;
276     } else {
277         return TRUE;
278     }
279 }
280
281 gchar *get_dlna_pn(gchar **additional_info_fields)
282 {
283     gchar *pn = NULL;
284     gint i;
285     for (i = 0; additional_info_fields[i]; i++) {
286         pn = g_strstr_len(additional_info_fields[i],
287                           strlen(additional_info_fields[i]), "DLNA.ORG_PN=");
288         if (pn) {
289             pn += 12; /* end of "DLNA.ORG_PN=" */
290             break;
291         }
292     }
293
294     return pn;
295 }
296
297 gboolean is_additional_info_compat(const gchar *renderer_additional_info,
298                                    const gchar *item_additional_info)
299 {
300     gboolean ret = FALSE;
301
302     if (g_ascii_strcasecmp(renderer_additional_info, "*") == 0) {
303         return TRUE;
304     }
305
306     char **renderer_tokens = g_strsplit(renderer_additional_info, ";", -1);
307     if (renderer_tokens == NULL) {
308         return FALSE;
309     }
310
311     char **item_tokens = g_strsplit(item_additional_info, ";", -1);
312     if (item_tokens == NULL) {
313         goto no_item_tokens;
314     }
315
316     char *renderer_pn = get_dlna_pn(renderer_tokens);
317     char *item_pn = get_dlna_pn(item_tokens);
318     if (renderer_pn == NULL || item_pn == NULL) {
319         goto no_renderer_pn;
320     }
321
322     if (g_ascii_strcasecmp(renderer_pn, item_pn) == 0) {
323         ret = TRUE;
324     }
325
326 no_renderer_pn:
327     g_strfreev(item_tokens);
328 no_item_tokens:
329     g_strfreev(renderer_tokens);
330
331     return ret;
332 }
333
334 gboolean is_protocol_info_compat(xmlNode *res_node,
335                                  const gchar *renderer_protocol)
336 {
337     gchar *item_protocol;
338     gchar **item_proto_tokens;
339     gchar **renderer_proto_tokens;
340     gboolean ret = FALSE;
341
342     item_protocol = gupnp_didl_lite_property_get_attribute(res_node,
343                                                            "protocolInfo");
344     if (!item_protocol) return FALSE;
345
346     item_proto_tokens = g_strsplit(item_protocol, ":", 4);
347     renderer_proto_tokens = g_strsplit(renderer_protocol, ":", 4);
348
349     if (!item_proto_tokens[0] || !item_proto_tokens[1] ||
350         !item_proto_tokens[2] || !item_proto_tokens[3] ||
351         !renderer_proto_tokens[0] || !renderer_proto_tokens[1] ||
352         !renderer_proto_tokens[2] || !renderer_proto_tokens[3])
353         goto return_point;
354
355     if (is_transport_compat(renderer_proto_tokens[0], renderer_proto_tokens[2],
356                             item_proto_tokens[0], item_proto_tokens[1]) &&
357         is_content_format_compat(renderer_proto_tokens[2],
358                                   item_proto_tokens[2]) &&
359         is_additional_info_compat(renderer_proto_tokens[3],
360                                   item_proto_tokens[3]))
361         ret = TRUE;
362
363 return_point:
364     g_free(item_protocol);
365     g_strfreev(renderer_proto_tokens);
366     g_strfreev(item_proto_tokens);
367
368     return ret;
369 }
370
371 char *find_compat_uri_from_metadata(const char *metadata, char **duration,
372                                     struct proxy *renderer)
373 {
374     char *uri = NULL;
375     void on_didl_item_available(GUPnPDIDLLiteParser *didl_parser,
376                                 xmlNode *item_node, gpointer user_data)
377     {
378         GList *resources =
379             gupnp_didl_lite_object_get_property(item_node, "res");
380         if (!resources) return;
381
382         int i;
383         for (i=0; renderer->protocols[i] && uri == NULL; i++) {
384             GList *res, *compat_res = NULL;
385             xmlNode *res_node;
386             for (res = resources; res != NULL; res = res->next) {
387                 res_node = (xmlNode *)res->data;
388
389                 int j;
390                 for (j=0; renderer->protocols[j]; j++) {
391                     if (is_protocol_info_compat(res_node,
392                                                 renderer->protocols[j])) {
393                         compat_res = res;
394                         break;
395                     }
396                 }
397             }
398             if (!compat_res) continue;
399
400             res_node = (xmlNode *)compat_res->data;
401             uri = gupnp_didl_lite_property_get_value(res_node);
402             *duration = gupnp_didl_lite_property_get_attribute(res_node,
403                                                                "duration");
404         }
405         g_list_free(resources);
406     }
407
408     GError *error = NULL;
409     /* Assumption: metadata only contains a single didl object */
410     gupnp_didl_lite_parser_parse_didl(renderer->set->didl_parser, metadata,
411                                       on_didl_item_available, NULL, &error);
412     if (error) {
413         g_warning("%s\n", error->message);
414         g_error_free(error);
415     }
416     
417     return uri;
418 }
419
420 struct proxy *current_renderer(struct proxy_set *proxy_set)
421 {
422     int i = hildon_picker_button_get_active(proxy_set->renderer_picker);
423     GtkTreeIter iter;
424     gtk_tree_model_iter_nth_child(GTK_TREE_MODEL(proxy_set->renderer_list),
425                                   &iter, NULL, i);
426     char *udn;
427     gtk_tree_model_get(GTK_TREE_MODEL(proxy_set->renderer_list), &iter,
428                        1, &udn, -1);
429     
430     return g_hash_table_lookup(proxy_set->renderers, udn);
431 }
432
433 void set_av_transport_uri(GUPnPServiceProxy *content_dir,
434                           GUPnPServiceProxyAction *action, gpointer user_data)
435 {
436     GError *error = NULL;
437     char *metadata;
438     gupnp_service_proxy_end_action(content_dir, action, &error,
439                                    "Result", G_TYPE_STRING, &metadata, NULL);
440     if (!metadata) return;
441     if (error) {
442         g_warning("Failed to get metadata for content");
443         g_error_free(error);
444     }
445
446     struct proxy_set *proxy_set = (struct proxy_set *)user_data;
447     struct proxy *renderer = current_renderer(proxy_set);
448
449     char *duration;
450     char *uri = find_compat_uri_from_metadata(metadata, &duration, renderer);
451     if (!uri) {
452         g_warning("no compatible URI found.");
453         return;
454     }
455
456     GUPnPServiceProxy *av_transport = GUPNP_SERVICE_PROXY(
457         gupnp_device_info_get_service(GUPNP_DEVICE_INFO(renderer->proxy),
458                                       AV_TRANSPORT));
459
460     gupnp_service_proxy_begin_action(av_transport, "SetAVTransportURI",
461                                          transport_uri, NULL,
462                                      "InstanceID", G_TYPE_UINT, 0,
463                                      "CurrentURI", G_TYPE_STRING, uri,
464                                      "CurrentURIMetaData", G_TYPE_STRING,
465                                          metadata,
466                                      NULL);
467 }
468
469 void av_transport_action_cb(GUPnPServiceProxy *av_transport,
470                             GUPnPServiceProxyAction *action, gpointer data)
471 {
472     const char *action_name = (const char *)data;
473     GError *error = NULL;
474
475     if (!gupnp_service_proxy_end_action(av_transport, action, &error, NULL)) {
476         g_warning("Failed to send action '%s': %s",
477                   action_name, error->message);
478         g_error_free(error);
479     }
480 }
481
482 void g_value_free(gpointer data)
483 {
484     g_value_unset((GValue *)data);
485     g_slice_free(GValue, data);
486 }
487
488 GHashTable *create_av_transport_args_hash(char **additional_args)
489 {
490     GHashTable *args = g_hash_table_new_full(g_str_hash, g_str_equal,
491                                              NULL, g_value_free);
492
493     GValue *instance_id = g_slice_alloc0(sizeof(GValue));
494     g_value_init(instance_id, G_TYPE_UINT);
495     g_value_set_uint(instance_id, 0);
496
497     g_hash_table_insert(args, "InstanceID", instance_id);
498
499     if (additional_args) {
500         int i;
501         for (i=0; additional_args[i]; i += 2) {
502             GValue *value = g_slice_alloc0(sizeof(GValue));
503             g_value_init(value, G_TYPE_STRING);
504             g_value_set_string(value, additional_args[i + 1]);
505             g_hash_table_insert(args, additional_args[i], value);
506         }
507     }
508     return args;
509 }
510
511 void av_transport_send_action(GUPnPServiceProxy *av_transport, char *action,
512                               char *additional_args[])
513 {
514     GHashTable *args = create_av_transport_args_hash(additional_args);
515
516     gupnp_service_proxy_begin_action_hash(av_transport, action,
517                                           av_transport_action_cb,
518                                           (char *)action,
519                                           args);
520     g_hash_table_unref(args);
521 }
522
523 void transport_selection(struct proxy *server, GtkTreeRowReference *row)
524 {
525     server->current_selection = row;
526     
527     GtkTreeModel *model = gtk_tree_row_reference_get_model(row);
528     GtkTreePath *path = gtk_tree_row_reference_get_path(row);
529
530     GtkTreeIter iter;
531     gtk_tree_model_get_iter(model, &iter, path);
532
533     char *label;
534     char *id;
535     GUPnPServiceProxy *content;
536     gtk_tree_model_get(model, &iter, COL_LABEL, &label,
537                                      COL_CONTENT, &content, COL_ID, &id, -1);
538     
539     gupnp_service_proxy_begin_action(content, "Browse",
540                                          set_av_transport_uri, server->set,
541                                      "ObjectID", G_TYPE_STRING, id,
542                                      "BrowseFlag", G_TYPE_STRING,
543                                          "BrowseMetadata",
544                                      "Filter", G_TYPE_STRING, "*",
545                                      "StartingIndex", G_TYPE_UINT, 0,
546                                      "RequestedCount", G_TYPE_UINT, 0,
547                                      "SortCriteria", G_TYPE_STRING, "",
548                                      NULL);
549 }
550
551 void play_button(GtkWidget *button, gpointer data)
552 {
553     struct proxy_set *proxy_set = (struct proxy_set *)data;
554     struct proxy *renderer = current_renderer(proxy_set);
555     GUPnPServiceProxy *av_transport = GUPNP_SERVICE_PROXY(
556         gupnp_device_info_get_service(GUPNP_DEVICE_INFO(renderer->proxy),
557                                       AV_TRANSPORT));
558     char *args[] = {"Speed", "1", NULL};
559     av_transport_send_action(av_transport, "Play", args);
560 }
561
562 GtkWidget *play_window(struct proxy *server, GtkTreeRowReference *row)
563 {
564     GtkWidget *window = hildon_stackable_window_new();
565     gtk_window_set_title(GTK_WINDOW(window), PLAY_WINDOW_TITLE);
566     
567     GtkWidget *vbox = gtk_vbox_new(FALSE, 0);
568     gtk_container_add(GTK_CONTAINER(window), vbox);
569     
570     GtkWidget *hbox = gtk_hbox_new(FALSE, 0);
571     gtk_box_pack_start(GTK_BOX(vbox), hbox, TRUE, TRUE, 6);
572     
573     GtkWidget *image = gtk_image_new();
574     gtk_box_pack_start(GTK_BOX(hbox), image, FALSE, FALSE, 6);
575     
576     GtkWidget *inner_box = gtk_vbox_new(FALSE, 0);
577     gtk_box_pack_start(GTK_BOX(hbox), inner_box, TRUE, TRUE, 6);
578
579     GtkWidget *button_box = gtk_hbutton_box_new();
580     gtk_box_pack_start(GTK_BOX(vbox), button_box, FALSE, FALSE, 6);
581     
582     GtkWidget *button =
583         hildon_button_new_with_text(HILDON_SIZE_FINGER_HEIGHT,
584                                     HILDON_BUTTON_ARRANGEMENT_VERTICAL,
585                                     "Play", "");
586     
587     g_signal_connect(button, "clicked", G_CALLBACK(play_button), server->set);
588     
589     gtk_box_pack_start(GTK_BOX(button_box), button, FALSE, FALSE, 0);
590     gtk_widget_show_all(GTK_WIDGET(vbox));
591     
592     return window;
593 }
594
595 void content_select(HildonTouchSelector *selector, gint column, gpointer data)
596 {
597     GtkTreePath *path;
598     path = hildon_touch_selector_get_last_activated_row(selector, column);
599     if (!path) return;
600
601     GtkTreeModel *model = hildon_touch_selector_get_model(selector, column);
602     GtkTreeIter iter;
603     gtk_tree_model_get_iter(model, &iter, path);
604
605     GUPnPServiceProxy *content;
606     char *id, *label;
607     gboolean container;
608     gtk_tree_model_get(model, &iter, COL_CONTENT, &content, COL_ID, &id,
609                                      COL_CONTAINER, &container,
610                                      COL_LABEL, &label, -1);
611
612     struct proxy *server = (struct proxy *)data;
613     GtkWidget *window;
614     
615     if (container) {
616         GtkListStore *view_list;
617         window = content_window(server, label, &view_list);
618         browse(content, id, 0, MAX_BROWSE, view_list, server->set->didl_parser);
619         gupnp_service_proxy_add_notify(content, "ContainerUpdateIDs",
620                                        G_TYPE_STRING, on_container_update_ids,
621                                        NULL);
622         gupnp_service_proxy_set_subscribed(content, TRUE);
623     } else {
624         GtkTreeRowReference *row = gtk_tree_row_reference_new(model, path);
625         transport_selection(server, row);
626         window = play_window(server, row);
627     }
628     
629     HildonWindowStack *stack = hildon_window_stack_get_default();
630     hildon_window_stack_push_1(stack, HILDON_STACKABLE_WINDOW(window));
631 }
632
633 GtkWidget *content_window(struct proxy *server, char *title,
634                           GtkListStore **view_list)
635 {
636     GtkWidget *window = hildon_stackable_window_new();
637     gtk_window_set_title(GTK_WINDOW(window), title);
638
639     GtkListStore *list = gtk_list_store_new(NUM_COLS, GDK_TYPE_PIXBUF,
640                                             G_TYPE_STRING, G_TYPE_STRING,
641                                             G_TYPE_POINTER, G_TYPE_BOOLEAN);
642
643     GtkWidget *selector = new_selector(list);
644
645     g_signal_connect(G_OBJECT(selector), "changed",
646                      G_CALLBACK(content_select), server);
647
648     gtk_container_add(GTK_CONTAINER(window), selector);
649
650     *view_list = list;
651     return window;
652 }
653
654 void server_select(HildonTouchSelector *selector, gint column, gpointer data)
655 {
656     GtkTreePath *path;
657     path = hildon_touch_selector_get_last_activated_row(selector, column);
658     if (!path) return;
659
660     GtkTreeModel *model = hildon_touch_selector_get_model(selector, column);
661     GtkTreeIter iter;
662     gtk_tree_model_get_iter(model, &iter, path);
663
664     char *udn;
665     gtk_tree_model_get(model, &iter, COL_ID, &udn, -1);
666
667     GHashTable *servers = (GHashTable *)data;
668     struct proxy *server = g_hash_table_lookup(servers, udn);
669
670     GUPnPServiceProxy *content_dir = GUPNP_SERVICE_PROXY(
671         gupnp_device_info_get_service(GUPNP_DEVICE_INFO(server->proxy),
672                                       CONTENT_DIR));
673
674     GtkListStore *view_list;
675     GtkWidget *window = content_window(server, server->name, &view_list);
676
677     browse(content_dir, "0", 0, MAX_BROWSE, view_list,
678            server->set->didl_parser);
679
680     gupnp_service_proxy_add_notify(content_dir, "ContainerUpdateIDs",
681                                    G_TYPE_STRING, on_container_update_ids,
682                                    NULL);
683     gupnp_service_proxy_set_subscribed(content_dir, TRUE);
684
685     /* TODO: GList *child_devices = gupnp_device_info_list_devices(GUPNP_DEVICE_INFO(server->proxy)); */
686
687     HildonWindowStack *stack = hildon_window_stack_get_default();
688     hildon_window_stack_push_1(stack, HILDON_STACKABLE_WINDOW(window));
689 }
690
691 GtkWidget *target_selector(struct proxy_set *proxy_set)
692 {
693     GtkWidget *selector;
694     selector = hildon_touch_selector_new();
695
696     HildonTouchSelectorColumn *column =
697         hildon_touch_selector_append_column(HILDON_TOUCH_SELECTOR(selector),
698             GTK_TREE_MODEL(proxy_set->renderer_list), NULL, NULL);
699     g_object_unref(proxy_set->renderer_list);
700     hildon_touch_selector_column_set_text_column(column, 0);
701
702     GtkCellRenderer *renderer = gtk_cell_renderer_text_new();
703     g_object_set(renderer, "xalign", 0.0, NULL);
704     gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(column), renderer, TRUE);
705     gtk_cell_renderer_set_fixed_size(renderer, -1, ROW_HEIGHT);
706     gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(column), renderer,
707                                    "text", 0, NULL);
708
709     gtk_widget_show_all(selector);
710     return selector;
711 }
712
713 GtkWidget *main_menu(struct proxy_set *proxy_set)
714 {
715     GtkWidget *menu = hildon_app_menu_new();
716     GtkWidget *button;
717     button = hildon_picker_button_new(HILDON_SIZE_AUTO,
718                                       HILDON_BUTTON_ARRANGEMENT_VERTICAL);
719     hildon_button_set_title(HILDON_BUTTON(button), CHOOSE_TARGET);
720     
721     GtkWidget *selector = target_selector(proxy_set);
722     hildon_picker_button_set_selector(HILDON_PICKER_BUTTON(button),
723                                       HILDON_TOUCH_SELECTOR(selector));
724     proxy_set->renderer_picker = HILDON_PICKER_BUTTON(button);
725     
726     hildon_app_menu_append(HILDON_APP_MENU(menu), GTK_BUTTON(button));
727
728     gtk_widget_show_all(menu);
729     return menu;
730 }
731
732 GtkWidget *main_window(HildonProgram *program, struct proxy_set *proxy_set)
733 {
734     GtkWidget *window = hildon_stackable_window_new();
735     hildon_program_add_window(program, HILDON_WINDOW(window));
736
737     gtk_window_set_title(GTK_WINDOW(window), APPLICATION_NAME);
738
739     GError *error = NULL;
740     proxy_set->icon = gtk_icon_theme_load_icon(gtk_icon_theme_get_default(),
741                                                "control_bluetooth_lan",
742                                                HILDON_ICON_PIXEL_SIZE_FINGER,
743                                                0, &error);
744     proxy_set->server_list = gtk_list_store_new(NUM_COLS, GDK_TYPE_PIXBUF,
745                                                 G_TYPE_STRING, G_TYPE_STRING,
746                                                 G_TYPE_POINTER, G_TYPE_BOOLEAN);
747
748     GtkWidget *selector = new_selector(proxy_set->server_list);
749
750     g_signal_connect(G_OBJECT(selector), "changed",
751                      G_CALLBACK(server_select), proxy_set->servers);
752
753     gtk_container_add(GTK_CONTAINER(window), selector);
754
755     GtkWidget *menu = main_menu(proxy_set);
756     hildon_window_set_app_menu(HILDON_WINDOW(window), HILDON_APP_MENU(menu));
757     
758     return window;
759 }
760
761 void protocol_info(GUPnPServiceProxy *cm, GUPnPServiceProxyAction *action,
762                    gpointer user_data)
763 {
764     gchar *sink_protocols;
765     GError *error = NULL;
766     const gchar *udn = gupnp_service_info_get_udn(GUPNP_SERVICE_INFO(cm));
767
768     if (!gupnp_service_proxy_end_action(cm, action, &error, "Sink",
769                                         G_TYPE_STRING, &sink_protocols,
770                                         NULL)) {
771         g_warning("Failed to get sink protocol info from "
772                        "media renderer '%s':%s\n",
773                    udn, error->message);
774         g_error_free(error);
775         return;
776     }
777
778     struct proxy *server = (struct proxy *)user_data;
779     server->protocols = g_strsplit(sink_protocols, ",", 0);
780 }
781
782 void add_renderer(GUPnPDeviceProxy *proxy, struct proxy_set *proxy_set)
783 {
784     const char *udn = gupnp_device_info_get_udn(GUPNP_DEVICE_INFO(proxy));
785     if (!udn) return;
786     
787     struct proxy *server = g_hash_table_lookup(proxy_set->renderers, udn);
788     if (server) return;
789     
790     char *name = gupnp_device_info_get_friendly_name(GUPNP_DEVICE_INFO(proxy));
791     if (!name) return;
792     
793     server = (struct proxy *)g_malloc(sizeof(struct proxy));
794     server->proxy = proxy;
795     server->name = name;
796     server->set = proxy_set;
797
798     GtkTreeIter iter;
799     gtk_list_store_append(proxy_set->renderer_list, &iter);
800     gtk_list_store_set(proxy_set->renderer_list, &iter,
801         0, server->name, 1, udn, -1);
802     
803     /* TODO: default to saved value */
804     hildon_picker_button_set_active(proxy_set->renderer_picker, 0);
805
806     GtkTreeModel *model = GTK_TREE_MODEL(proxy_set->renderer_list);
807     GtkTreePath *path = gtk_tree_model_get_path(model, &iter);
808     server->row = gtk_tree_row_reference_new(model, path);
809
810     g_hash_table_replace(proxy_set->renderers, (char *)udn, server);
811
812     GUPnPServiceProxy *cm = GUPNP_SERVICE_PROXY(
813         gupnp_device_info_get_service(GUPNP_DEVICE_INFO(proxy),
814                                       CONNECTION_MANAGER));
815     gupnp_service_proxy_begin_action(cm, "GetProtocolInfo", protocol_info,
816                                      server, NULL);
817 }
818
819 void add_server(GUPnPDeviceProxy *proxy, struct proxy_set *proxy_set)
820 {
821     const char *udn = gupnp_device_info_get_udn(GUPNP_DEVICE_INFO(proxy));
822     if (!udn) return;
823     
824     struct proxy *server = g_hash_table_lookup(proxy_set->servers, udn);
825     if (server) return;
826
827     char *name = gupnp_device_info_get_friendly_name(GUPNP_DEVICE_INFO(proxy));
828     GUPnPServiceInfo *content_dir =
829         gupnp_device_info_get_service(GUPNP_DEVICE_INFO(proxy), CONTENT_DIR);
830     
831     if (!name || !content_dir) return;
832
833     server = (struct proxy *)g_malloc(sizeof(struct proxy));
834     server->proxy = proxy;
835     server->name = name;
836     server->set = proxy_set;
837     
838     GtkTreeIter iter;
839     gtk_list_store_append(proxy_set->server_list, &iter);
840     gtk_list_store_set(proxy_set->server_list, &iter,
841         COL_ICON, proxy_set->icon, COL_LABEL, server->name, COL_ID, udn,
842         COL_CONTENT, NULL, COL_CONTAINER, TRUE, -1);
843
844     GtkTreeModel *model = GTK_TREE_MODEL(proxy_set->server_list);
845     GtkTreePath *path = gtk_tree_model_get_path(model, &iter);
846     server->row = gtk_tree_row_reference_new(model, path);
847
848     g_hash_table_replace(proxy_set->servers, (char *)udn, server);
849 }
850
851 void remove_renderer(GUPnPDeviceProxy *proxy, struct proxy_set *proxy_set)
852 {
853     const char *udn = gupnp_device_info_get_udn(GUPNP_DEVICE_INFO(proxy));
854     struct proxy *server = g_hash_table_lookup(proxy_set->renderers, udn);
855     if (!server) return;
856
857     GtkTreeModel *model = GTK_TREE_MODEL(proxy_set->renderer_list);
858     GtkTreeIter iter;
859     gtk_tree_model_get_iter(model, &iter,
860                             gtk_tree_row_reference_get_path(server->row));
861     gtk_list_store_remove(proxy_set->renderer_list, &iter);
862     g_hash_table_remove(proxy_set->renderers, udn);
863     free(server);
864     
865     /* TODO: change current selection if necessary */
866 }
867
868 void remove_server(GUPnPDeviceProxy *proxy, struct proxy_set *proxy_set)
869 {
870     const char *udn = gupnp_device_info_get_udn(GUPNP_DEVICE_INFO(proxy));
871     struct proxy *server = g_hash_table_lookup(proxy_set->servers, udn);
872     if (!server) return;
873
874     GtkTreeModel *model = GTK_TREE_MODEL(proxy_set->server_list);
875     GtkTreeIter iter;
876     gtk_tree_model_get_iter(model, &iter,
877                             gtk_tree_row_reference_get_path(server->row));
878     gtk_list_store_remove(proxy_set->server_list, &iter);
879     g_hash_table_remove(proxy_set->servers, udn);
880     free(server);
881     
882     /* TODO: bring user back to server menu if necessary */
883 }
884
885 void device_proxy_available(GUPnPControlPoint *cp,
886                             GUPnPDeviceProxy *proxy, gpointer user_data)
887 {
888     struct proxy_set *proxy_set = (struct proxy_set *)user_data;
889     const char *type;
890     type = gupnp_device_info_get_device_type(GUPNP_DEVICE_INFO(proxy));
891
892     if (g_pattern_match_simple(MEDIA_RENDERER, type)) {
893         add_renderer(proxy, proxy_set);
894     } else if (g_pattern_match_simple(MEDIA_SERVER, type)) {
895         add_server(proxy, proxy_set);
896     }
897 }
898
899 void device_proxy_unavailable(GUPnPControlPoint *cp,
900                               GUPnPDeviceProxy *proxy, gpointer user_data)
901 {
902     struct proxy_set *proxy_set = (struct proxy_set *)user_data;
903     const char *type;
904     type = gupnp_device_info_get_device_type(GUPNP_DEVICE_INFO(proxy));
905
906     if (g_pattern_match_simple(MEDIA_RENDERER, type)) {
907         remove_renderer(proxy, proxy_set);
908     } else if (g_pattern_match_simple(MEDIA_SERVER, type)) {
909         remove_server(proxy, proxy_set);
910     }
911 }
912
913 void init_upnp(struct proxy_set *proxy_set)
914 {
915     GError *error = NULL;
916
917     g_type_init();
918
919     GUPnPContext *context = gupnp_context_new(NULL, NULL, 0, &error);
920     if (error) {
921         g_printerr("Error creating the GUPnP context: %s\n", error->message);
922         g_error_free(error);
923         exit(1);
924     }
925
926     GUPnPControlPoint *cp = gupnp_control_point_new(context, "ssdp:all");
927
928     g_signal_connect(cp, "device-proxy-available",
929                      G_CALLBACK(device_proxy_available), proxy_set);
930     g_signal_connect(cp, "device-proxy-unavailable",
931                      G_CALLBACK(device_proxy_unavailable), proxy_set);
932
933     gssdp_resource_browser_set_active(GSSDP_RESOURCE_BROWSER(cp), TRUE);
934 }
935
936 int main(int argc, char *argv[])
937 {
938     g_thread_init(NULL);
939
940     hildon_gtk_init(&argc, &argv);
941
942     HildonProgram *program = hildon_program_get_instance();
943     g_set_application_name(APPLICATION_NAME);
944
945     struct proxy_set proxy_set;
946     proxy_set.renderers = g_hash_table_new(g_str_hash, g_str_equal);
947     proxy_set.servers = g_hash_table_new(g_str_hash, g_str_equal);
948
949     proxy_set.renderer_list = gtk_list_store_new(2, G_TYPE_STRING,
950                                                     G_TYPE_STRING);
951     proxy_set.didl_parser = gupnp_didl_lite_parser_new();
952     GtkWidget *window = main_window(program, &proxy_set);
953     g_signal_connect(G_OBJECT(window), "destroy",
954                      G_CALLBACK(gtk_main_quit), NULL);
955     
956     init_upnp(&proxy_set);
957
958     gtk_widget_show(window);
959     gtk_main();
960
961     return 0;
962 }